ci: fix the failing job, split deploy out, and drop the Android build
All checks were successful
Forgejo Docker Build / Root app tests (push) Successful in 54s
Forgejo Docker Build / Build Docker image (push) Successful in 7s

Three things, one subject: making CI say the truth about this repo.

## The red on every run was ours, not the runners'

Every docker-build run came back success, success, failure — the same
shape for weeks. The failing job was `deploy`, and it was failing to
*not run*:

    if: ${{ github.event.inputs.deploy == 'true' }}

On a push there is no github.event.inputs at all. This Forgejo does not
treat that as false and skip; it dispatches the job, the runner cannot
resolve it, and the task ends in "Early termination". The runners were
never at fault, and nothing about them needed changing.

The `'runs-on' key not defined` line is a red herring: the `build` job
prints it too and succeeds. It names the job's *needs* target, not the
job, and the old android-apk workflow used `needs:` happily for months.

Deploy is now its own workflow with only workflow_dispatch — no
condition to evaluate, so nothing can be dispatched by mistake. No job
in either file now carries a job-level `if`. The one conditional left is
a *step* (push to registry), and step conditions are evaluated by the
runner once the job is already running, which is why that one has always
worked.

## dev and main

docker-build now runs on `dev` as well. Both branches prove the same two
things — tests pass, image builds — and only `main` publishes the image,
so nothing on `dev` can be mistaken for something deployable. Deploying
stays a person pressing a button after looking at the change.
CONTRIBUTING.md documents the flow.

## Android

Removed: the mobile/ Capacitor project, docs/mobile-build.md, and the
Android bits of scripts/release.sh. All of it is in git history — 4613a278
is the last commit that had it — for when it is rebuilt.

src/utils/platform.js stays. isMobileClient only decides token lifetime,
it is twelve lines, and it is the contract a future app would come back
to; deleting it would be a change to auth for no gain.

.github/workflows/ went too — all five. There is no GitHub remote on
this repository, so none of them has ever run, and two of them wrote
into mobile/ paths that no longer exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-12 23:30:52 +02:00
parent ceccd18387
commit 36cb742ce7
115 changed files with 134 additions and 5324 deletions

View file

@ -0,0 +1,55 @@
name: Deploy
# Its own workflow, and workflow_dispatch only — there is no push trigger, so
# there is nothing to skip. Deploying used to be a job inside the build
# workflow behind `if: github.event.inputs.deploy == 'true'`. On a push there
# is no github.event.inputs at all; this Forgejo dispatched the job regardless,
# the runner could not resolve it, and it reported "Early termination". Every
# run of that workflow showed a failure for a job that was never meant to run.
#
# A separate file also matches what deploying is: a deliberate act, taken after
# someone has looked at the change, not a consequence of pushing.
on:
workflow_dispatch:
inputs:
revision:
description: Full commit SHA to deploy. Leave blank for the branch tip.
required: false
default: ''
jobs:
deploy:
name: Deploy to the host
runs-on: forgejo-local
env:
DEPLOY_DIR: ${{ vars.DEPLOY_DIR || '/home/danvics/docker/ped-ai' }}
steps:
# The deploy directory is also a working tree. This refuses rather than
# resetting over someone's uncommitted work.
- name: Refuse to deploy over uncommitted work
run: |
if [ -n "$(git -C "$DEPLOY_DIR" status --porcelain)" ]; then
echo "$DEPLOY_DIR has uncommitted changes; commit or stash them first." >&2
git -C "$DEPLOY_DIR" status --short >&2
exit 1
fi
# Detaches HEAD at the deployed revision, which is what a deployed tree
# should be. If DEPLOY_DIR is also where you write code, point this at a
# checkout of its own instead — vars.DEPLOY_DIR.
- name: Move the deploy checkout to this revision
run: |
REVISION="${{ github.event.inputs.revision }}"
[ -n "$REVISION" ] || REVISION="${{ github.sha }}"
echo "REVISION=$REVISION" >> "$GITHUB_ENV"
git -C "$DEPLOY_DIR" fetch --quiet --all
git -C "$DEPLOY_DIR" checkout --quiet --detach "$REVISION"
# deploy.sh pins the image, waits for health, asks /api/build what is
# actually running, and rolls back if it disagrees. Schema migrations are
# applied by the container's own entrypoint before the app starts.
- name: Deploy and verify
run: |
IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3"
"$DEPLOY_DIR/scripts/deploy.sh" "$IMAGE:$REVISION" "$REVISION"

View file

@ -1,18 +1,21 @@
name: Forgejo Docker Build
# dev proves the tests pass and the image builds. main additionally publishes
# the image, because main is what production runs.
#
# Deploying is a separate workflow, not a job here behind an "if". A job whose
# "if" is false is still dispatched to a runner by this Forgejo, which then
# cannot resolve it and reports "Early termination" — that was the failure on
# every run of this workflow. Nothing here now depends on a job being skipped.
on:
push:
branches: [main]
branches: [main, dev]
workflow_dispatch:
inputs:
push_image:
description: Push image to Forgejo container registry
description: Push image to Forgejo container registry (main only)
required: false
default: 'true'
deploy:
description: Deploy the built image to the host after pushing
required: false
default: 'false'
jobs:
root-test:
@ -57,8 +60,11 @@ jobs:
docker tag ped-ai-local:latest "$IMAGE:$REVISION"
docker tag ped-ai-local:latest "$IMAGE:latest"
# A step, not a job. Step conditions are evaluated by the runner once the
# job is already running, so a false one simply skips — it cannot produce
# the dispatch failure a job-level condition does here.
- name: Push image to Forgejo registry
if: ${{ github.event.inputs.push_image != 'false' }}
if: ${{ github.ref == 'refs/heads/main' && github.event.inputs.push_image != 'false' }}
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
run: |
@ -67,43 +73,3 @@ jobs:
echo "$FORGEJO_TOKEN" | docker login git.danvics.com -u danvics --password-stdin
docker push "$IMAGE:$REVISION"
docker push "$IMAGE:latest"
# ── Deploy ──────────────────────────────────────────────────────────────
# Opt-in, because the deploy directory is also a working tree: this refuses to
# run if it has uncommitted changes rather than resetting over someone's work.
#
# It moves that checkout to the built revision first, so the Compose file and
# the entrypoint that ship with the image are the ones used to run it, then
# hands over to scripts/deploy.sh, which pins the image, waits for health,
# asks /api/build what is actually running and rolls back if it disagrees.
# Schema migrations are applied by the container's own entrypoint.
deploy:
needs: build
name: Deploy to the host
runs-on: forgejo-local
if: ${{ github.event.inputs.deploy == 'true' }}
env:
DEPLOY_DIR: ${{ vars.DEPLOY_DIR || '/home/danvics/docker/ped-ai' }}
steps:
- name: Refuse to deploy over uncommitted work
run: |
if [ -n "$(git -C "$DEPLOY_DIR" status --porcelain)" ]; then
echo "$DEPLOY_DIR has uncommitted changes; commit or stash them first." >&2
git -C "$DEPLOY_DIR" status --short >&2
exit 1
fi
# Detaches HEAD at the deployed revision, which is what you want a
# deployed tree to be. If DEPLOY_DIR is also where you write code, point
# it at a checkout of its own instead — vars.DEPLOY_DIR.
- name: Move the deploy checkout to this revision
run: |
REVISION="${{ github.sha }}"
git -C "$DEPLOY_DIR" fetch --quiet --all
git -C "$DEPLOY_DIR" checkout --quiet --detach "$REVISION"
- name: Deploy and verify
run: |
IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3"
REVISION="${{ github.sha }}"
"$DEPLOY_DIR/scripts/deploy.sh" "$IMAGE:$REVISION" "$REVISION"

View file

@ -1,148 +0,0 @@
name: Auto version & release
# Fires on every push to main. Parses commit messages since the
# last semver tag, decides patch/minor/major bump, creates the
# tag, pushes. The tag push then triggers android-release.yml and
# docker-publish.yml. Fully hands-off — you never pick a version
# number; your commit messages do.
#
# Commit message grammar (Conventional Commits):
# feat: → minor bump (new feature, backward-compatible)
# fix: → patch bump (bug fix)
# feat!: / BREAKING CHANGE in body → major bump
# everything else (docs, refactor, chore, style, ci, test) → no bump
#
# Skip conditions (no new release created):
# - No commits match the above patterns
# - The most recent commit is itself a release commit ("Release v…")
# - [skip ci] appears in any commit message since the last tag
on:
push:
branches: [main]
# Opt in to Node 24 runtime early (deprecation of Node 20 begins 2026-06-02)
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
permissions:
contents: write
jobs:
version:
runs-on: ubuntu-latest
if: "github.server_url == 'https://github.com' && !contains(github.event.head_commit.message, 'Release v') && !contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Use RELEASE_PAT (a Personal Access Token you add as a repo
# secret) so the tag push this workflow performs actually
# triggers the downstream tag-based workflows (android-release,
# docker-publish). GITHUB_TOKEN pushes are deliberately
# blocked from triggering other workflows by GitHub.
# Fine-grained PAT with "Contents: Read and write" on this
# repo is enough.
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
- name: Find last semver tag
id: last
run: |
LAST=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1)
if [[ -z "$LAST" ]]; then
LAST="v0.0.0"
echo "no previous tag, starting from v0.0.0"
fi
echo "tag=$LAST"
echo "tag=$LAST" >> "$GITHUB_OUTPUT"
echo "version=${LAST#v}" >> "$GITHUB_OUTPUT"
- name: Decide bump type from commit messages
id: decide
env:
LAST: ${{ steps.last.outputs.tag }}
run: |
# All commits from the last tag → HEAD (exclusive of tag commit)
if [[ "$LAST" == "v0.0.0" ]]; then
MSGS=$(git log --format='%s%n%b%n---')
else
MSGS=$(git log "${LAST}..HEAD" --format='%s%n%b%n---')
fi
BUMP=none
if echo "$MSGS" | grep -qE '(^|\n)(BREAKING CHANGE:|[a-z]+(\([^)]+\))?!:)'; then
BUMP=major
elif echo "$MSGS" | grep -qE '(^|\n)feat(\([^)]+\))?: '; then
BUMP=minor
elif echo "$MSGS" | grep -qE '(^|\n)fix(\([^)]+\))?: '; then
BUMP=patch
fi
echo "Bump type decided: $BUMP"
echo "bump=$BUMP" >> "$GITHUB_OUTPUT"
{
echo "### Commits since $LAST"
echo '```'
if [[ "$LAST" == "v0.0.0" ]]; then
git log --oneline | head -20
else
git log "${LAST}..HEAD" --oneline
fi
echo '```'
echo ""
echo "**Bump decision**: \`$BUMP\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Stop if no release-worthy commits
if: steps.decide.outputs.bump == 'none'
run: |
echo "No feat / fix / BREAKING commits since last tag — not cutting a release."
echo "::notice::No release cut. Commit with 'feat:', 'fix:', or BREAKING CHANGE to trigger one."
- name: Compute next version
id: next
if: steps.decide.outputs.bump != 'none'
env:
CUR: ${{ steps.last.outputs.version }}
BUMP: ${{ steps.decide.outputs.bump }}
run: |
IFS='.' read -r MAJ MIN PAT <<< "$CUR"
case "$BUMP" in
major) NEXT="$((MAJ+1)).0.0" ;;
minor) NEXT="${MAJ}.$((MIN+1)).0" ;;
patch) NEXT="${MAJ}.${MIN}.$((PAT+1))" ;;
esac
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
echo "### Next version: v$NEXT" >> "$GITHUB_STEP_SUMMARY"
- name: Configure git
if: steps.decide.outputs.bump != 'none'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Bump version strings + tag + push
if: steps.decide.outputs.bump != 'none'
env:
V: ${{ steps.next.outputs.next }}
run: |
IFS='.' read -r MAJ MIN PAT <<< "$V"
ANDROID_CODE=$(( MAJ * 100000 + MIN * 1000 + PAT ))
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" package.json
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" mobile/package.json
sed -i -E \
-e "s/versionCode +[0-9]+/versionCode ${ANDROID_CODE}/" \
-e "s/versionName +\"[^\"]+\"/versionName \"${V}\"/" \
mobile/android/app/build.gradle
git add package.json mobile/package.json mobile/android/app/build.gradle
git commit -m "Release v${V}"
git tag -a "v${V}" -m "Release v${V}"
git push origin HEAD
git push origin "v${V}"
echo "### Released v$V" >> "$GITHUB_STEP_SUMMARY"
echo "android-release + docker-publish workflows will now run." >> "$GITHUB_STEP_SUMMARY"

View file

@ -1,34 +0,0 @@
name: CI
# Runs root app tests on every PR and push to main.
on:
pull_request:
push:
branches: [main]
# Cancel superseded runs on the same ref to save minutes.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Root app tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node 24
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install
run: npm ci
- name: Unit tests
run: npm test

View file

@ -1,156 +0,0 @@
name: Build & Push Docker Image
# Multi-arch build using NATIVE runners for each platform, then a
# manifest-list push. No QEMU emulation — amd64 builds on x86 runner,
# arm64 builds on ubuntu-24.04-arm runner. argon2 and every other
# native dep compile natively on their target arch.
#
# Result: `danielonyejesi/pediatric-ai-scribe-v3:X.Y.Z` (and :latest)
# is one tag serving the correct variant to amd64 or arm64 hosts.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Tag to publish (e.g. v6.2.0)'
required: false
default: 'latest'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
IMAGE: danielonyejesi/pediatric-ai-scribe-v3
jobs:
root-test:
if: ${{ github.server_url == 'https://github.com' }}
name: Root app tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm test
build:
needs: root-test
if: ${{ github.server_url == 'https://github.com' }}
# Build one variant per matrix entry, push by digest only.
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker metadata (for labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & push by digest
id: build
uses: docker/build-push-action@v5
with:
context: .
build-args: |
GIT_REVISION=${{ github.sha }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
- name: Export digest for the merge job
run: |
mkdir -p /tmp/digests
DIG="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${DIG#sha256:}"
- name: Upload digest artifact
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
if: ${{ github.server_url == 'https://github.com' }}
# Combine the two single-platform digests into one multi-arch manifest
# published under the real tags (vX.Y.Z and latest).
name: Merge manifests
needs: build
runs-on: ubuntu-latest
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Resolve tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag || 'latest' }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
fi
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=${{ steps.tag.outputs.tag }}
type=raw,value=latest
- name: Create manifest list & push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "${{ env.IMAGE }}@sha256:%s " *)
- name: Inspect final image
run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}
- name: Summary
run: |
echo "### Multi-arch image published" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.IMAGE }}:latest\`" >> $GITHUB_STEP_SUMMARY
echo "- Platforms: linux/amd64, linux/arm64 (built on native runners)" >> $GITHUB_STEP_SUMMARY

View file

@ -1,30 +0,0 @@
name: Security audit
# Weekly npm audit at high+ severity for the root app. Reports to the job summary; does NOT fail the build
# (advisories appear constantly and a red checkmark train would just get
# muted). Re-run on demand via workflow_dispatch.
on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
workflow_dispatch:
jobs:
audit:
name: npm audit (high+)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node 24
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Audit root app
run: |
echo '## Root app advisories' >> "$GITHUB_STEP_SUMMARY"
npm audit --audit-level=high --json > legacy-audit.json || true
node -e "const a=require('./legacy-audit.json');const m=a.metadata?.vulnerabilities||{};console.log('high:'+(m.high||0)+' critical:'+(m.critical||0));" >> "$GITHUB_STEP_SUMMARY"
continue-on-error: true

View file

@ -1,103 +0,0 @@
name: Version bump & release
# Manual trigger — click "Run workflow" in the Actions tab, choose
# patch / minor / major. The workflow computes the next semver,
# updates package.json, mobile/package.json, and the Android
# build.gradle, commits the change, tags it, and pushes — which
# triggers the android-release and docker-publish workflows.
on:
workflow_dispatch:
inputs:
bump:
description: 'Semver bump type'
required: true
type: choice
default: patch
options:
- patch
- minor
- major
custom:
description: 'Or exact version (e.g. 7.0.0) — overrides bump'
required: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
permissions:
contents: write
jobs:
bump:
if: ${{ github.server_url == 'https://github.com' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
- name: Compute next version
id: v
run: |
CUR=$(grep -m1 '"version"' package.json | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
echo "current=$CUR"
IFS='.' read -r MAJ MIN PAT <<< "$CUR"
if [[ -n "${{ github.event.inputs.custom }}" ]]; then
NEXT="${{ github.event.inputs.custom }}"
else
case "${{ github.event.inputs.bump }}" in
major) NEXT="$((MAJ+1)).0.0" ;;
minor) NEXT="${MAJ}.$((MIN+1)).0" ;;
patch) NEXT="${MAJ}.${MIN}.$((PAT+1))" ;;
esac
fi
if ! [[ "$NEXT" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::invalid version: $NEXT"; exit 1
fi
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
echo "current=$CUR" >> "$GITHUB_OUTPUT"
echo "### Version bump" >> "$GITHUB_STEP_SUMMARY"
echo "- Current: $CUR" >> "$GITHUB_STEP_SUMMARY"
echo "- Next: $NEXT" >> "$GITHUB_STEP_SUMMARY"
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Bump version strings
env:
V: ${{ steps.v.outputs.next }}
run: |
IFS='.' read -r MAJ MIN PAT <<< "$V"
ANDROID_CODE=$(( MAJ * 100000 + MIN * 1000 + PAT ))
# package.json (top-level "version": "...")
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" package.json
sed -i -E "0,/(\"version\"[[:space:]]*:[[:space:]]*\")[^\"]+(\")/ s//\1${V}\2/" mobile/package.json
# Android
sed -i -E \
-e "s/versionCode +[0-9]+/versionCode ${ANDROID_CODE}/" \
-e "s/versionName +\"[^\"]+\"/versionName \"${V}\"/" \
mobile/android/app/build.gradle
git diff --stat
- name: Commit, tag, push
env:
V: ${{ steps.v.outputs.next }}
run: |
git add package.json mobile/package.json mobile/android/app/build.gradle
git commit -m "Release v${V}"
git tag -a "v${V}" -m "Release v${V}"
git push origin HEAD
git push origin "v${V}"
echo "### Pushed" >> "$GITHUB_STEP_SUMMARY"
echo "- tag: v${V}" >> "$GITHUB_STEP_SUMMARY"
echo "- android-release + docker-publish workflows will now run" >> "$GITHUB_STEP_SUMMARY"

View file

@ -1,11 +1,11 @@
# Contributing
<!-- Pipeline verified 2026-04-15: auto-version + PAT + multi-arch docker -->
## Commit format
[Conventional Commits](https://www.conventionalcommits.org). `.github/workflows/auto-version.yml`
parses messages since the last semver tag and decides whether to bump.
[Conventional Commits](https://www.conventionalcommits.org). Nothing parses
these automatically any more — the auto-version workflow was a GitHub one and
this repository has no GitHub remote — but the prefixes still say what a change
is, and `scripts/release.sh` still wants a version chosen the same way.
| Prefix | Bump | |
|---|---|---|
@ -14,21 +14,47 @@ parses messages since the last semver tag and decides whether to bump.
| `feat!:` / `fix!:` / `BREAKING CHANGE:` in body | major | breaking change |
| `docs:` `refactor:` `chore:` `test:` `style:` `ci:` `build:` | none | no release |
Append `[skip ci]` to suppress the run for that commit.
## Manual release
```bash
scripts/release.sh 6.2.0 --push # local
scripts/release.sh 6.2.0 --push # bump, commit, tag, push
```
or Actions tab → **Version bump & release** → Run workflow → pick bump type.
## Branches
## What a tag push triggers
`main` is production. It is what `scripts/deploy.sh` deploys and what the
container registry publishes from. `dev` is where work lands first.
| Workflow | Output |
|---|---|
| `docker-publish.yml` | `danielonyejesi/pediatric-ai-scribe-v3:{version,latest}` on Docker Hub (amd64) |
```
feature work ──▶ dev ──▶ (tests pass, you try it) ──▶ main ──▶ deploy
```
| Branch | On push, CI does | Publishes an image |
|---|---|---|
| `dev` | runs the test suite, then builds the image | no |
| `main` | runs the test suite, builds the image, pushes it to the registry | yes |
`dev` builds the image but does not publish it, so nothing on `dev` can be
mistaken for something deployable. Both branches prove the same two things —
the tests pass and the image builds — which is the point: by the time a change
reaches `main` the only new question is whether it is *right*, not whether it
works mechanically.
Deploying is never automatic. It is the **Deploy** workflow, run by hand from
the Actions tab, after you have looked at the change. That is deliberate: the
step between "tests pass" and "this is live" is a person deciding, and a push
is not a decision. `scripts/deploy.sh` then pins the image, waits for health,
asks `/api/build` which revision is actually serving, and rolls back if the
answer disagrees.
To merge up:
```bash
git checkout main && git merge --no-ff dev && git push forgejo main
```
`--no-ff` keeps the merge visible, so a release is one commit to point at and
one commit to revert.
## Local dev
@ -43,10 +69,6 @@ automatically on fresh page loads).
Server code changes require `./scripts/build-image.sh && docker compose up -d --no-build`.
## Mobile
See `docs/mobile-build.md`.
## DB migrations
`src/db/database.js` is the baseline (idempotent CREATE-IF-NOT-EXISTS). New

View file

@ -175,7 +175,6 @@ Primary references:
- `docs/deployment.md` — production deployment.
- `docs/scaling.md` — scaling priorities and readiness work.
- `docs/openid-setup.md` — OIDC provider setup.
- `docs/mobile-build.md` — the Capacitor wrapper and app-store build notes.
- `docs/ops-docs-ped-ai-and-milvus.md` — operational notes for the retrieval stack.
- `docs/improvements.md` — the running list of what to improve next.
- `docs/logic/README.md` — the deeper code walkthrough.

16
TODO.md
View file

@ -59,16 +59,16 @@ and green (674 tests, three consecutive clean runs).
## Deployment readiness (CI/CD and Kubernetes)
What already exists: `ci.yml` (tests on PR and main), `security.yml` (weekly
audit), `docker-publish.yml` and Android/versioning workflows, a Dockerfile
`HEALTHCHECK`, and `/api/health`.
What already exists: `.forgejo/workflows/docker-build.yml` (tests then image,
on `dev` and `main`; publishes from `main` only), `.forgejo/workflows/deploy.yml`
(manual dispatch), a Dockerfile `HEALTHCHECK`, and `/api/health`. The GitHub
workflows were removed — this repository has no GitHub remote, so none of them
ever ran.
Worth doing before Kubernetes, roughly in order:
- [ ] **Fail CI on vulnerabilities.** `security.yml` runs weekly and does not
gate merges. `npm audit --audit-level=high` in `ci.yml` would have caught
the nodemailer advisories at the PR.
- [ ] **Build the image on PRs too.** `docker-publish.yml` only runs on tags, so
a Dockerfile break is found at release time.
- [ ] **Fail CI on vulnerabilities.** Nothing runs `npm audit` any more; the
weekly GitHub job went with the rest. `npm audit --audit-level=high` as a
step in `docker-build.yml` would have caught the nodemailer advisories.
- [ ] **Run the e2e suite in CI.** `scripts/e2e.sh` and
`docker-compose.e2e.yml` exist but nothing calls them.
- [ ] **Separate liveness from readiness.** `/api/health` is one endpoint;

View file

@ -61,21 +61,9 @@ public/ # SPA
components/ # per-tab HTML fragments
css/styles.css
mobile/ # Capacitor wrapper
capacitor.config.json # appId com.pedshub.scribe
src/ # launcher (server-URL picker)
android/ # generated AS project + native Java
.forgejo/workflows/
android-apk.yml # signed APK on tag push; optional Play upload
docker-build.yml # Forgejo registry Docker image build
.github/workflows/
auto-version.yml # conventional-commits → semver bump → tag
android-release.yml # legacy GitHub tag APK release path
docker-publish.yml # multi-arch image on tag push
version-bump.yml # manual dispatch override
build-apk.yml # legacy TWA APK
docker-build.yml # dev + main: tests, image; main also publishes
deploy.yml # manual dispatch: deploy.sh against the host
```
## Request pipeline

View file

@ -222,15 +222,15 @@ Container marked unhealthy after 5 failures.
## CI / CD
On push (and tag push), these workflows run (depending on runner/site):
Forgejo Actions only; there is no GitHub remote on this repository.
| Workflow | Output | Runtime |
| Workflow | Trigger | What it does |
|---|---|---|
| `docker-publish.yml` | Multi-arch image (amd64 + arm64 via native runners) on Docker Hub | ~4 min |
| `.forgejo/workflows/docker-build.yml` | push to `dev` or `main` | test suite, then build the image. On `main` only, push it to `git.danvics.com/danvics/pediatric-ai-scribe-v3:{revision,latest}` |
| `.forgejo/workflows/deploy.yml` | manual dispatch | `scripts/deploy.sh` against the host: pin the image, wait for health, verify `/api/build`, roll back on disagreement |
Triggered by `auto-version.yml` (reads commit messages, bumps + tags via
`RELEASE_PAT`) or manually via `Actions → Version bump & release` or
`scripts/release.sh X.Y.Z --push`.
Deploying is never automatic — see "Branches" in `CONTRIBUTING.md`. Versioning
is manual: `scripts/release.sh X.Y.Z --push`.
## Ports

View file

@ -50,8 +50,7 @@ public/
css/styles.css
template-guide.md downloadable user template guide
mobile/ Capacitor 6 wrapper (Android + iOS)
.github/workflows/ CI (auto-version, APK, docker)
.forgejo/workflows/ CI (tests + image on dev/main; manual deploy)
```
## Backend

View file

@ -1,129 +0,0 @@
# Mobile Build And Release
> **The CI build has been removed.** `.forgejo/workflows/android-apk.yml` and
> the two GitHub APK workflows are gone: CI now covers the Docker image and the
> web app only. The Capacitor project under `mobile/` is still here and still
> builds by hand with the Gradle commands below — nothing builds it for you.
Capacitor 6 wrapper around the hosted Ped-AI web app. The launcher defaults to `https://app.pedshub.com`, lets the user change the server URL, and stores that URL locally. Android is buildable on Linux. The iOS project exists but requires macOS and Xcode to produce an `.ipa`.
This is not a separate native clinical app. The native shell provides WebView hosting, microphone permission plumbing, secure storage, and mobile packaging for the same authenticated web app.
## One-time setup
### Keystore
```bash
keytool -genkeypair -v -keystore ~/pedscribe-release.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias pedscribe
```
Store the password in a password manager. Back up the `.jks` file off the
machine. Losing it = can't sign updates; Play Store requires signature
continuity (unless you're on Play App Signing).
### Android Studio (optional, IDE workflow only)
```bash
export CAPACITOR_ANDROID_STUDIO_PATH="/snap/android-studio/current/bin/studio.sh"
npx cap open android
```
Required repo secrets (set once, via Settings → Secrets and variables → Actions
or `gh secret set`):
- `ANDROID_KEYSTORE_BASE64``base64 -w0 ~/pedscribe-release.jks`
- `ANDROID_KEYSTORE_PASSWORD`
- `ANDROID_KEY_ALIAS``pedscribe`
- `ANDROID_KEY_PASSWORD`
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_B64` — base64 of your Google Play service
account JSON (optional). If present, the same tag build also runs `bundleRelease`
and uploads the AAB to Play's `internal` track.
Optional Play Store flow:
- Service account must have permissions to edit releases on the app in Play.
- Build task is `bundleRelease`, tracked as `com.pedshub.scribe`.
- Upload lane is `fastlane/android publish_internal` (under `mobile/android/fastlane`).
Tag a release:
```bash
# conventional-commits prefix auto-tags (see CONTRIBUTING.md)
git commit -m "feat: ..." && git push # auto-version workflow bumps minor
git commit -m "fix: ..." && git push # auto-version workflow bumps patch
# or force an exact version
scripts/release.sh X.Y.Z --push
```
APK lands on the Forgejo release. Obtainium can still track
`git.danvics.com/danvics/pediatric-ai-scribe-v3` releases automatically.
Play Store upload is handled automatically for tagged builds only when
`GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_B64` is configured.
## Local build (fallback / debugging)
```bash
cd mobile
npm install
npx cap sync android
cd android
./gradlew assembleRelease \
-Pandroid.injected.signing.store.file=$HOME/pedscribe-release.jks \
-Pandroid.injected.signing.store.password='<pass>' \
-Pandroid.injected.signing.key.alias=pedscribe \
-Pandroid.injected.signing.key.password='<pass>'
```
Output: `android/app/build/outputs/apk/release/app-release.apk`
For Play Store, swap `assembleRelease``bundleRelease`; output: `.aab` under
`bundle/release/`.
If web assets or Capacitor config changed, run `npx cap sync android` from `mobile/` before building.
### Single-quote the password
Keystore passwords with shell metacharacters (`)`, `$`, `!`, space, etc.) must
be single-quoted. Backslash line continuations get eaten by some terminal
paste handlers — prefer one-line commands.
## Reinstall on device
```bash
adb install -r android/app/build/outputs/apk/release/app-release.apk
```
`-r` keeps app data (saved server URL, auth token in Keystore, IndexedDB).
## Gotchas
- **JDK 17 only.** Newer JDK (21/25) breaks Android Gradle Plugin. Set
`org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64` in `~/.gradle/gradle.properties`
if the system default is different.
- **QEMU multi-arch Docker builds fail** with SIGILL on native modules (argon2).
Docker Hub workflow is x86-only. Use a native ARM runner if you need ARM64.
- **`npx cap` must run inside `mobile/`**, not repo root.
- **Foreground recording on Android 14+** requires `foregroundServiceType="microphone"`
in `AndroidManifest.xml` plus the 3-arg `startForeground(id, notif, TYPE_MICROPHONE)`.
Already applied.
- **Mic "denied" after permission grant** — WebView intercepts the prompt.
Fix: long-press app icon → App info → Permissions → Microphone → Allow.
## Files
Note on `com.pedshub.scribe`: that's the Android applicationId — the OS-level
unique identifier for this app. Chosen by reverse-DNS of `pedshub.com`. It is
**not** a reference to the separate PedsHub Quiz app; they share a prefix by
coincidence. Don't rename it — Android treats applicationId as the primary
key; renaming breaks Play Store update continuity and forces every installed
user to uninstall + reinstall.
| Path | Purpose |
|---|---|
| `mobile/capacitor.config.json` | appId, name, WebView config, plugin opts |
| `mobile/src/` | launcher HTML and server URL entry, defaulting to `https://app.pedshub.com` |
| `mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java` | JS bridge + WebView mic permission |
| `mobile/android/app/src/main/java/com/pedshub/scribe/AudioRecordingService.java` | foreground service for background recording |
| `mobile/android/app/src/main/AndroidManifest.xml` | permissions, intents, backup rules |
| `mobile/android/fastlane/Fastfile` | internal Play track upload lane |

36
mobile/.gitignore vendored
View file

@ -1,36 +0,0 @@
# Node / npm — keep package-lock.json for reproducible CI builds,
# ignore only the installed tree.
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Capacitor generated files (rewritten by `npx cap sync`)
# Keep the *project* (mobile/android/, mobile/ios/) but not the
# per-sync mirrors.
android/app/src/main/assets/public/
android/app/src/main/assets/capacitor.config.json
android/app/src/main/assets/capacitor.plugins.json
android/app/capacitor.build.gradle
android/capacitor.settings.gradle
android/capacitor-cordova-android-plugins/
ios/App/App/public/
ios/App/capacitor-cordova-ios-plugins/
ios/App/Pods/
ios/App/Podfile.lock
# Android build outputs & local state
android/.gradle/
android/build/
android/app/build/
android/app/release/
android/local.properties
android/app/release/output-metadata.json
android/.idea/
*.apk
*.aab
*.jks
# macOS
.DS_Store

View file

@ -1,156 +0,0 @@
# PedScribe Mobile App
Capacitor mobile wrapper for the hosted Ped-AI web app. The app defaults to `https://app.pedshub.com`, lets users choose a self-hosted server URL, and keeps clinical workflows API-backed through the same Express service as the browser app.
## Features
- Hosted web workflow inside a native WebView; server updates reach mobile clients without app-store releases
- Configurable server URL (supports self-hosted instances)
- Haptic feedback on recording start/stop
- Keep screen awake during recording
- Deep linking (pedscribe:// and https://app.pedshub.com)
- Share intent (receive text/PDFs from other apps)
- Push notification support
- **Biometric sign-in** (Face ID / Touch ID / fingerprint) — credentials
stored in iOS Keychain / Android Keystore, gated by OS biometric.
Enrolled on first password sign-in (opt-in prompt). 2FA still applies
on top — biometric replaces the password step only.
- Android and iOS project scaffolds for store builds
## Prerequisites
- Node.js 18+
- Android Studio (for Android builds): `sudo snap install android-studio --classic`
- Xcode 15+ (for iOS builds, macOS only)
- Apple Developer account ($99/yr for App Store)
- Google Play Developer account ($25 one-time)
## Setup
```bash
cd mobile
npm install
npx cap sync
```
## Build Android
```bash
# Open in Android Studio
npx cap open android
# Build menu: Build > Generate Signed Bundle / APK > APK
# Sign with your keystore (create one on first build)
# APK output: android/app/build/outputs/apk/release/
# Or build from command line:
cd android && ./gradlew assembleRelease
```
## Build iOS (macOS only)
```bash
# Open in Xcode
npx cap open ios
# In Xcode:
# 1. Select your team/signing certificate
# 2. Product > Archive
# 3. Distribute App > App Store Connect
```
## How It Works
1. App launches with a local launcher page
2. First launch: user enters their PedScribe server URL (default: app.pedshub.com)
3. URL is saved locally for future launches
4. App navigates to the remote web app inside a native WebView
5. Native plugins provide background recording, haptics, and push notifications
### Background Recording
**Android:** `AudioRecordingService` is a foreground service that:
- Acquires a partial wake lock (CPU stays active, screen can sleep)
- Shows a persistent notification ("Recording in progress...")
- Includes a "Stop Recording" quick action in the notification
- Maximum 1-hour wake lock duration
**iOS:** Uses `UIBackgroundModes: audio` in Info.plist, which tells iOS to keep the app alive for audio capture when backgrounded or screen-locked.
### Deep Linking
- `pedscribe://` custom URL scheme opens the app directly
- `https://app.pedshub.com` links open in the app instead of the browser (Android App Links)
### Share Intent (Android)
Other apps can share text or PDFs directly into PedScribe:
- Share a lab result from your email into the Chart Review tab
- Share a referral note into the Hospital Course tab
## Capacitor Plugins Included
| Plugin | Purpose |
|--------|---------|
| @capacitor/app | App lifecycle management |
| @capacitor/haptics | Vibration feedback on recording start/stop |
| @capacitor/keyboard | Keyboard management for WebView |
| @capacitor/push-notifications | Push notification support |
| @capacitor/screen-orientation | Screen orientation control |
| @capacitor/share | Native share dialog |
| @capacitor/splash-screen | Launch splash screen |
| @capacitor/status-bar | Status bar styling |
## App Structure
```
mobile/
capacitor.config.json # Capacitor configuration
package.json # Dependencies
src/
index.html # Launcher page (server URL config)
launcher.js # Auto-redirect + native feature init
launcher.css # Launcher styles
android/ # Android native project
app/src/main/
java/com/pedshub/scribe/
MainActivity.java
AudioRecordingService.java
AndroidManifest.xml # Permissions, deep links, share intent
ios/ # iOS native project
App/App/
Info.plist # Background audio, microphone, deep links
```
## Updating the Web App
The mobile app wraps the remote web app — updating the server automatically updates all mobile clients. No app store update needed for web changes.
To update native features (plugins, permissions, splash screen):
```bash
cd mobile
npm install
npx cap sync
# Then rebuild in Android Studio / Xcode
```
## Generating App Icons
Replace the default Capacitor icons with PedScribe branding:
1. Create a 1024x1024 PNG icon
2. Install the assets tool: `npm install -D @capacitor/assets`
3. Place your icon as `assets/icon-only.png` and `assets/splash.png`
4. Run: `npx capacitor-assets generate`
This generates all required sizes for both platforms.
## App Store Listing Suggestions
**Title:** PedScribe - Pediatric AI Scribe
**Subtitle:** Voice-to-Note Clinical Documentation
**Category:** Medical
**Keywords:** pediatric, scribe, medical, documentation, HPI, SOAP, clinical, AI, voice
**Description:**
PedScribe is an AI-powered clinical documentation tool for pediatric physicians. Record patient encounters, and the AI generates structured medical notes — HPIs, SOAP notes, hospital courses, chart reviews, and more. Includes pediatric calculators, developmental milestone tracking, and a learning hub with quizzes. Self-hosted for maximum privacy with HIPAA-compliant AI providers.

View file

@ -1,101 +0,0 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml

View file

@ -1,2 +0,0 @@
/build/*
!/build/.npmkeep

View file

@ -1,57 +0,0 @@
apply plugin: 'com.android.application'
android {
namespace "com.pedshub.scribe"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.pedshub.scribe"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
// Version values below are overwritten by scripts/release.sh from
// the root package.json. versionCode auto-increments per release.
versionCode 714016
versionName "7.14.16"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
implementation "androidx.biometric:biometric:1.2.0-alpha05"
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,26 +0,0 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Biometric login (capacitor-native-biometric). USE_BIOMETRIC is the
API 28+ permission; older devices ignore it. No legacy FINGERPRINT
entry needed because capacitor-native-biometric targets API 23+. -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep linking: pedscribe:// and https://app.pedshub.com -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="pedscribe" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="app.pedshub.com" />
</intent-filter>
<!-- Share intent: receive text/files from other apps -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/pdf" />
</intent-filter>
</activity>
<service
android:name=".AudioRecordingService"
android:foregroundServiceType="microphone"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>

View file

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>PedScribe</title>
<link rel="stylesheet" href="launcher.css">
</head>
<body>
<div class="launcher">
<!-- Auto-redirect screen (shown when server URL is saved) -->
<div id="connecting-screen" style="display:none;">
<div class="logo-icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
</svg>
</div>
<h1>PedScribe</h1>
<p class="subtitle">Connecting...</p>
<div class="spinner"></div>
<button id="btn-change-server" class="btn-link">Change Server</button>
</div>
<!-- Server URL setup screen -->
<div id="setup-screen">
<div class="logo-icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
</svg>
</div>
<h1>PedScribe</h1>
<p class="subtitle">AI-Powered Pediatric Clinical Documentation</p>
<div class="form-group">
<label>Server URL</label>
<input type="url" id="server-url" placeholder="https://app.pedshub.com" autocapitalize="none" autocorrect="off" spellcheck="false">
</div>
<button id="btn-connect" class="btn-primary">
Connect
</button>
<p class="hint">Enter the URL of your Pediatric AI Scribe server. If you don't have one, use the default.</p>
<div class="footer">
<p>Pediatric AI Scribe by PedsHub</p>
<p>Committed to healthcare equity</p>
</div>
</div>
</div>
<script src="launcher.js"></script>
</body>
</html>

View file

@ -1,134 +0,0 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #1d4ed8 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
.launcher {
width: 100%;
max-width: 400px;
padding: 40px 24px;
text-align: center;
}
.logo-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
}
.logo-icon svg { width: 100%; height: 100%; }
h1 {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.subtitle {
font-size: 14px;
opacity: 0.7;
margin-bottom: 32px;
}
.form-group {
text-align: left;
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
opacity: 0.8;
margin-bottom: 6px;
}
.form-group input {
width: 100%;
padding: 14px 16px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 12px;
background: rgba(255,255,255,0.15);
color: white;
font-size: 16px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.form-group input::placeholder { color: rgba(255,255,255,0.4); }
.form-group input:focus { border-color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.2); }
.btn-primary {
width: 100%;
padding: 14px;
border: none;
border-radius: 12px;
background: white;
color: #1d4ed8;
font-size: 16px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
}
.btn-primary:active { transform: scale(0.98); }
.btn-primary:disabled { opacity: 0.5; }
.btn-link {
background: none;
border: none;
color: rgba(255,255,255,0.6);
font-size: 13px;
cursor: pointer;
margin-top: 16px;
font-family: inherit;
text-decoration: underline;
}
.hint {
margin-top: 20px;
font-size: 12px;
opacity: 0.5;
line-height: 1.5;
}
.footer {
margin-top: 40px;
font-size: 11px;
opacity: 0.3;
line-height: 1.6;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(255,255,255,0.2);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 20px auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Error state */
.error-msg {
background: rgba(239,68,68,0.2);
border: 1px solid rgba(239,68,68,0.4);
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
margin-top: 12px;
display: none;
}

View file

@ -1,70 +0,0 @@
// PedScribe Mobile Launcher
// Handles configurable server URL and auto-redirect
(function() {
var STORAGE_KEY = 'pedscribe_server_url';
var DEFAULT_URL = 'https://app.pedshub.com';
var setupScreen = document.getElementById('setup-screen');
var connectingScreen = document.getElementById('connecting-screen');
var urlInput = document.getElementById('server-url');
var connectBtn = document.getElementById('btn-connect');
var changeBtn = document.getElementById('btn-change-server');
var savedUrl = localStorage.getItem(STORAGE_KEY);
if (savedUrl) {
showConnecting(savedUrl);
} else {
urlInput.value = DEFAULT_URL;
showScreen('setup');
}
// Connect button
connectBtn.addEventListener('click', function() {
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
if (!url.startsWith('http')) url = 'https://' + url;
connectBtn.disabled = true;
connectBtn.textContent = 'Connecting...';
haptic();
localStorage.setItem(STORAGE_KEY, url);
navigateToServer(url);
});
urlInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') connectBtn.click();
});
// Change server
changeBtn.addEventListener('click', function() {
localStorage.removeItem(STORAGE_KEY);
urlInput.value = savedUrl || DEFAULT_URL;
showScreen('setup');
urlInput.focus();
});
// Screen management
function showScreen(which) {
setupScreen.style.display = which === 'setup' ? '' : 'none';
connectingScreen.style.display = which === 'connecting' ? '' : 'none';
}
function showConnecting(url) {
showScreen('connecting');
setTimeout(function() { navigateToServer(url); }, 800);
}
function navigateToServer(url) {
window.location.href = url;
}
function haptic() {
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
window.Capacitor.Plugins.Haptics.impact({ style: 'medium' });
}
} catch(e) {}
}
})();

View file

@ -1,113 +0,0 @@
package com.pedshub.scribe;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import androidx.core.app.NotificationCompat;
/**
* Foreground service that keeps the app alive during audio recording.
* Acquires a partial wake lock to prevent CPU sleep during recording.
* The Capacitor web app sends a message to start/stop this service when recording.
*/
public class AudioRecordingService extends Service {
private static final String CHANNEL_ID = "recording_channel";
private static final int NOTIFICATION_ID = 1;
private static final String WAKE_LOCK_TAG = "PedScribe:AudioRecording";
public static final String ACTION_STOP = "com.pedshub.scribe.STOP_RECORDING";
private PowerManager.WakeLock wakeLock;
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
stopSelf();
return START_NOT_STICKY;
}
// Acquire wake lock to keep CPU active during recording.
// 8h cap is a safety net onDestroy() releases early when recording
// stops. The cap prevents a runaway lock if the service leaks.
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
wakeLock.acquire(8 * 60 * 60 * 1000L);
}
// Stop action in notification
Intent stopIntent = new Intent(this, AudioRecordingService.class);
stopIntent.setAction(ACTION_STOP);
PendingIntent stopPending = PendingIntent.getService(
this, 0, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Pediatric AI Scribe")
.setContentText("Recording in progress...")
.setSmallIcon(android.R.drawable.ic_btn_speak_now)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.addAction(android.R.drawable.ic_media_pause, "Stop Recording", stopPending)
.build();
// Android 14 (SDK 34) requires the 3-arg form with an explicit
// foregroundServiceType matching the manifest declaration, else
// the service is killed with MissingForegroundServiceTypeException.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else {
startForeground(NOTIFICATION_ID, notification);
}
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
stopForeground(STOP_FOREGROUND_REMOVE);
super.onDestroy();
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"Recording",
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Shows when audio recording is active");
channel.setShowBadge(false);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
}

View file

@ -1,322 +0,0 @@
package com.pedshub.scribe;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintManager;
import android.provider.MediaStore;
import android.util.Base64;
import android.view.WindowManager;
import android.webkit.CookieManager;
import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;
import android.webkit.WebViewClient;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.getcapacitor.BridgeActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class MainActivity extends BridgeActivity {
private static final int MIC_PERMISSION_CODE = 1001;
private PermissionRequest pendingPermissionRequest;
private WebView printWebView;
// True between startForegroundService() and stopForegroundService(), i.e.
// while the web app has an active MediaRecorder. Drives the keep-screen-on
// flag and the timer-throttling workaround below.
private volatile boolean recordingActive = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Request mic permission upfront
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{ Manifest.permission.RECORD_AUDIO }, MIC_PERMISSION_CODE);
}
// Allow the Cloudflare Turnstile iframe to use storage.
setupThirdPartyCookies();
// Setup WebView mic permission granting
setupWebViewPermissions();
// Register JS interface for foreground service control
setupRecordingBridge();
// Register JS interface for Android's print / Save as PDF flow.
setupPrintBridge();
// Register JS interface for saving generated visuals to Photos.
setupFileBridge();
}
// Recording Lifecycle
//
// Recording happens in the WebView (MediaRecorder), not in native code,
// so keeping the foreground service alive is necessary but not sufficient
// the WebView also has to keep executing JS. Two things protect that:
//
// 1. FLAG_KEEP_SCREEN_ON while recording, so the device does not
// auto-lock mid-encounter. This is the case that actually bites
// clinicians: a long pause in conversation and the screen times out.
//
// 2. resumeTimers() if the activity is paused anyway (user presses the
// power button, or a call comes in). Chromium throttles timers hard
// for hidden WebViews, which starves MediaRecorder's chunk delivery.
// Capacitor never calls webView.onPause(), so the WebView itself is
// still live it is only the timers that need rescuing.
//
// Note resumeTimers()/pauseTimers() are process-global in WebView, not
// per-instance; calling resume here is safe because this app has no other
// WebView that wants throttling (printWebView is transient).
void setKeepScreenOn(final boolean on) {
runOnUiThread(() -> {
if (on) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
void setRecordingActive(boolean active) {
recordingActive = active;
setKeepScreenOn(active);
}
// NB: BridgeActivity declares these public narrowing to protected would
// not compile.
@Override
public void onPause() {
super.onPause();
if (recordingActive && this.bridge != null && this.bridge.getWebView() != null) {
this.bridge.getWebView().resumeTimers();
}
}
@Override
public void onResume() {
super.onResume();
if (this.bridge != null && this.bridge.getWebView() != null) {
this.bridge.getWebView().resumeTimers();
}
}
// Third-Party Cookies
//
// Android WebView blocks third-party cookies by default (unlike Chrome,
// which still allows them for now). Cloudflare Turnstile runs inside a
// cross-origin iframe from challenges.cloudflare.com and needs its own
// storage to run and persist a challenge without this the widget
// silently stalls or errors and never emits a token, so registration and
// password reset are impossible from inside the app.
//
// This is scoped to our own WebView, which only ever loads the PedScribe
// origin (see allowNavigation in capacitor.config.json), so it is not a
// general relaxation of the app's cookie policy.
private void setupThirdPartyCookies() {
WebView webView = this.bridge.getWebView();
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setAcceptThirdPartyCookies(webView, true);
}
// WebView Microphone Permission
private void setupWebViewPermissions() {
WebView webView = this.bridge.getWebView();
final MainActivity activity = this;
webView.setWebChromeClient(new WebChromeClient() {
@Override
public void onPermissionRequest(final PermissionRequest request) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {
activity.runOnUiThread(() -> request.grant(request.getResources()));
} else {
pendingPermissionRequest = request;
ActivityCompat.requestPermissions(activity,
new String[]{ Manifest.permission.RECORD_AUDIO }, MIC_PERMISSION_CODE);
}
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == MIC_PERMISSION_CODE && pendingPermissionRequest != null) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
final PermissionRequest req = pendingPermissionRequest;
runOnUiThread(() -> req.grant(req.getResources()));
} else {
pendingPermissionRequest.deny();
}
pendingPermissionRequest = null;
}
}
// Background Recording Service Bridge
private void setupRecordingBridge() {
WebView webView = this.bridge.getWebView();
webView.addJavascriptInterface(new RecordingBridge(this), "NativeRecording");
}
private void setupPrintBridge() {
WebView webView = this.bridge.getWebView();
webView.addJavascriptInterface(new PrintBridge(this), "NativePrint");
}
private void setupFileBridge() {
WebView webView = this.bridge.getWebView();
webView.addJavascriptInterface(new FileBridge(this), "NativeFiles");
}
public static class RecordingBridge {
private final MainActivity activity;
RecordingBridge(MainActivity activity) {
this.activity = activity;
}
@android.webkit.JavascriptInterface
public void startForegroundService() {
Intent intent = new Intent(activity, AudioRecordingService.class);
ContextCompat.startForegroundService(activity, intent);
activity.setRecordingActive(true);
}
@android.webkit.JavascriptInterface
public void stopForegroundService() {
Intent intent = new Intent(activity, AudioRecordingService.class);
intent.setAction(AudioRecordingService.ACTION_STOP);
activity.startService(intent);
activity.setRecordingActive(false);
}
// Standalone keep-awake, exposed so the web app can hold the screen on
// for non-recording work too. window.nativeKeepAwake() previously
// called Capacitor's KeepAwake plugin, which is not installed in this
// project so it silently did nothing and the screen slept during
// recordings.
@android.webkit.JavascriptInterface
public void keepAwake(boolean on) {
activity.setKeepScreenOn(on);
}
}
public static class PrintBridge {
private final MainActivity activity;
PrintBridge(MainActivity activity) {
this.activity = activity;
}
@android.webkit.JavascriptInterface
public void printHtml(String title, String base64Html) {
activity.runOnUiThread(() -> activity.printHtmlFromBase64(title, base64Html));
}
}
public static class FileBridge {
private final MainActivity activity;
FileBridge(MainActivity activity) {
this.activity = activity;
}
@android.webkit.JavascriptInterface
public String saveImage(String filename, String base64Png) {
return activity.saveImageToPictures(filename, base64Png);
}
}
private void printHtmlFromBase64(String title, String base64Html) {
try {
byte[] decoded = Base64.decode(base64Html, Base64.DEFAULT);
String html = new String(decoded, java.nio.charset.StandardCharsets.UTF_8);
printWebView = new WebView(this);
printWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
PrintDocumentAdapter adapter = view.createPrintDocumentAdapter(title != null && !title.isEmpty() ? title : "Clinical Assistant Export");
printManager.print(title != null && !title.isEmpty() ? title : "Clinical Assistant Export", adapter, new PrintAttributes.Builder().build());
}
});
printWebView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
} catch (Exception e) {
android.util.Log.e("PedScribe", "Native print failed", e);
}
}
private String saveImageToPictures(String filename, String base64Png) {
String safeName = sanitizeFilename(filename, "clinical-visual.png");
try {
byte[] imageBytes = Base64.decode(base64Png, Base64.DEFAULT);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, safeName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/PedScribe");
values.put(MediaStore.Images.Media.IS_PENDING, 1);
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri == null) return "error:Could not create image file";
try (OutputStream out = resolver.openOutputStream(uri)) {
if (out == null) return "error:Could not open image file";
out.write(imageBytes);
}
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(uri, values, null, null);
} else {
File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "PedScribe");
if (!dir.exists() && !dir.mkdirs()) return "error:Could not create Pictures/PedScribe";
File file = new File(dir, safeName);
try (OutputStream out = new FileOutputStream(file)) {
out.write(imageBytes);
}
uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
}
return "saved:" + uri.toString();
} catch (Exception e) {
android.util.Log.e("PedScribe", "Native image save failed", e);
return "error:" + (e.getMessage() != null ? e.getMessage() : "Image save failed");
}
}
private String sanitizeFilename(String filename, String fallback) {
String value = filename != null ? filename : fallback;
value = value.replaceAll("[^A-Za-z0-9._-]", "-");
if (value.length() == 0) value = fallback;
if (!value.toLowerCase(java.util.Locale.US).endsWith(".png")) value = value + ".png";
return value;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View file

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#2563eb</color>
<color name="colorPrimaryDark">#1d4ed8</color>
<color name="colorAccent">#2563eb</color>
</resources>

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View file

@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">PedScribe</string>
<string name="title_activity_main">PedScribe</string>
<string name="package_name">com.pedshub.scribe</string>
<string name="custom_url_scheme">com.pedshub.scribe</string>
</resources>

View file

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@color/colorPrimary</item>
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View file

@ -1,6 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
</widget>

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Android 12+ backup rules. Exclude everything from both cloud backup
and device-to-device transfer. The app handles PHI-adjacent data
(Keychain/Keystore-backed auth token, cached web content) that must
not leave the device via Google's automatic backup system.
-->
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</device-transfer>
</data-extraction-rules>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
<files-path name="my_files" path="." />
<external-files-path name="my_external_files" path="." />
</paths>

View file

@ -1,18 +0,0 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View file

@ -1,29 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View file

@ -1,2 +0,0 @@
json_key_file('fastlane/google-play-service-account.json')
package_name('com.pedshub.scribe')

View file

@ -1,18 +0,0 @@
default_platform(:android)
platform :android do
desc "Upload a signed release AAB to Google Play internal track"
lane :publish_internal do
upload_to_play_store(
package_name: 'com.pedshub.scribe',
json_key: 'fastlane/google-play-service-account.json',
aab: ENV['AAB_PATH'] || 'app/build/outputs/bundle/release/app-release.aab',
track: ENV['PLAY_TRACK'] || 'internal',
skip_upload_changelogs: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true,
release_status: 'completed',
)
end
end

View file

@ -1,3 +0,0 @@
source 'https://rubygems.org'
gem 'fastlane'

View file

@ -1,22 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

Binary file not shown.

View file

@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
mobile/android/gradlew vendored
View file

@ -1,248 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View file

@ -1,92 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -1,5 +0,0 @@
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
apply from: 'capacitor.settings.gradle'

View file

@ -1,16 +0,0 @@
ext {
minSdkVersion = 22
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1'
}

View file

@ -1,39 +0,0 @@
{
"appId": "com.pedshub.scribe",
"appName": "PedScribe",
"webDir": "src",
"server": {
"allowNavigation": [
"app.pedshub.com",
"*.pedshub.com",
"pedshub.com",
"peds.danvics.com"
]
},
"plugins": {
"SplashScreen": {
"launchShowDuration": 1500,
"backgroundColor": "#2563eb",
"showSpinner": true,
"spinnerColor": "#ffffff"
},
"PushNotifications": {
"presentationOptions": ["badge", "sound", "alert"]
},
"Keyboard": {
"resize": "body",
"resizeOnFullScreen": true
}
},
"android": {
"allowMixedContent": false,
"backgroundColor": "#2563eb",
"webContentsDebuggingEnabled": false,
"appendUserAgent": "PedScribe-Android",
"androidScheme": "https"
},
"ios": {
"backgroundColor": "#2563eb",
"contentInset": "automatic"
}
}

13
mobile/ios/.gitignore vendored
View file

@ -1,13 +0,0 @@
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml

View file

@ -1,408 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
);
name = Products;
sourceTree = "<group>";
};
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
);
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
504EC3021FED79650016851F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC30C1FED79650016851F /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC3111FED79650016851F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
504EC3151FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.pedshub.scribe;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.pedshub.scribe;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -1,49 +0,0 @@
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

View file

@ -1,14 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-512@2x.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -1,23 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "splash-2732x2732-2.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View file

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</imageView>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="Splash" width="1366" height="1366"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
</dependencies>
<scenes>
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View file

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>PedScribe</string>
<key>NSFaceIDUsageDescription</key>
<string>PedScribe uses Face ID to securely sign you in without re-entering your password.</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>PedScribe needs microphone access to record clinical encounters for AI-powered documentation.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>remote-notification</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>pedscribe</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -1,51 +0,0 @@
{
"appId": "com.pedshub.scribe",
"appName": "PedScribe",
"webDir": "src",
"server": {
"allowNavigation": [
"*"
]
},
"plugins": {
"SplashScreen": {
"launchShowDuration": 1500,
"backgroundColor": "#2563eb",
"showSpinner": true,
"spinnerColor": "#ffffff"
},
"PushNotifications": {
"presentationOptions": [
"badge",
"sound",
"alert"
]
},
"Keyboard": {
"resize": "body",
"resizeOnFullScreen": true
}
},
"android": {
"allowMixedContent": true,
"backgroundColor": "#2563eb",
"webContentsDebuggingEnabled": true,
"appendUserAgent": "PedScribe-Android",
"androidScheme": "https"
},
"ios": {
"backgroundColor": "#2563eb",
"contentInset": "automatic"
},
"packageClassList": [
"AppPlugin",
"BiometricAuthNative",
"HapticsPlugin",
"KeyboardPlugin",
"PushNotificationsPlugin",
"ScreenOrientationPlugin",
"SharePlugin",
"SplashScreenPlugin",
"StatusBarPlugin"
]
}

View file

@ -1,6 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
</widget>

View file

View file

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>PedScribe</title>
<link rel="stylesheet" href="launcher.css">
</head>
<body>
<div class="launcher">
<!-- Auto-redirect screen (shown when server URL is saved) -->
<div id="connecting-screen" style="display:none;">
<div class="logo-icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
</svg>
</div>
<h1>PedScribe</h1>
<p class="subtitle">Connecting...</p>
<div class="spinner"></div>
<button id="btn-change-server" class="btn-link">Change Server</button>
</div>
<!-- Server URL setup screen -->
<div id="setup-screen">
<div class="logo-icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
</svg>
</div>
<h1>PedScribe</h1>
<p class="subtitle">AI-Powered Pediatric Clinical Documentation</p>
<div class="form-group">
<label>Server URL</label>
<input type="url" id="server-url" placeholder="https://app.pedshub.com" autocapitalize="none" autocorrect="off" spellcheck="false">
</div>
<button id="btn-connect" class="btn-primary">
Connect
</button>
<p class="hint">Enter the URL of your Pediatric AI Scribe server. If you don't have one, use the default.</p>
<div class="footer">
<p>Pediatric AI Scribe by PedsHub</p>
<p>Committed to healthcare equity</p>
</div>
</div>
</div>
<script src="launcher.js"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more