-
Notifications
You must be signed in to change notification settings - Fork 0
feat(field-ops): deployable PWA — offline photo fix, login, R2 storage, fail-closed prod gate #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6ec795b
40c30e1
584b671
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,9 @@ dist/ | |
| htmlcov/ | ||
| *.bak | ||
|
|
||
| # Node / frontend | ||
| node_modules/ | ||
|
|
||
| # OS / Editor | ||
| .DS_Store | ||
| .AppleDouble | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,247 @@ | ||||||||||
| # Deploying Field Ops for the Palawan fieldwork | ||||||||||
|
|
||||||||||
| **Written 2026-08-06.** Target: a URL that field staff can open on their phones | ||||||||||
| before they leave, and that keeps working when they have no signal. | ||||||||||
|
|
||||||||||
| ``` | ||||||||||
| Vercel → the PWA (static build) | ||||||||||
| Fly / Railway → FastAPI container (the existing Dockerfile) | ||||||||||
| Neon → Postgres | ||||||||||
| Cloudflare R2 → photo blobs | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| **Nothing here creates accounts or handles credentials on your behalf.** Every | ||||||||||
| step you must do yourself is marked **[you]**. Values go into each platform's | ||||||||||
| secret store; none of them belong in this repo. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 0. Read this before provisioning anything | ||||||||||
|
|
||||||||||
| Three things were broken for field use and are now fixed. They are worth knowing | ||||||||||
| because they change what you should test: | ||||||||||
|
|
||||||||||
| | Was | Now | | ||||||||||
| |---|---| | ||||||||||
| | **No login screen at all** — `login()` existed but nothing called it; the only way in was to hand-craft a JWT into `localStorage` | Real sign-in form; one account per observer | | ||||||||||
| | **Offline submissions destroyed the photo** — the blob was never queued, and `reset()` cleared it, while the UI said "Saved offline" | Photo is queued in IndexedDB with the record and uploaded on sync | | ||||||||||
| | **Photos written to local disk** — fine on a laptop, gone on the next container restart | Configurable backend; **production refuses to start unless it is R2** | | ||||||||||
|
|
||||||||||
| The Queue tab now lists what is on the device and what has synced, so an | ||||||||||
| operator can confirm their day's work exists before leaving a site. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 1. Neon — Postgres **[you]** | ||||||||||
|
|
||||||||||
| 1. Create a project at neon.tech. Region: **Singapore** (`ap-southeast-1`) — the | ||||||||||
| closest to Palawan; every extra 100 ms shows up on a weak link. | ||||||||||
| 2. Copy the connection string. It looks like | ||||||||||
| `postgresql://user:pass@ep-xxx.ap-southeast-1.aws.neon.tech/neondb?sslmode=require` | ||||||||||
| 3. Keep it in your password manager. It is a credential — do not paste it into | ||||||||||
| chat, a commit, or a screenshot. | ||||||||||
|
|
||||||||||
| **Use the pooled endpoint** (Neon shows it as "Pooled connection", host contains | ||||||||||
| `-pooler`). A container that restarts holds connections open; the direct | ||||||||||
| endpoint runs out. | ||||||||||
|
|
||||||||||
| `?sslmode=require` and `channel_binding` are stripped automatically — asyncpg | ||||||||||
| rejects libpq's parameters and the resulting error is not obvious. | ||||||||||
|
|
||||||||||
| ### Run the migrations **[you]** | ||||||||||
|
|
||||||||||
| From this repo, with `DATABASE_URL` exported in your shell: | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| export DATABASE_URL='postgresql://...' # from Neon, pooled | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== file outline / relevant lines =="
wc -l services/field-ops/DEPLOY.md
sed -n '1,150p' services/field-ops/DEPLOY.md
echo
echo "== credential occurrences =="
rg -n "DATABASE_URL|R2_ACCESS_KEY_ID|R2_SECRET_ACCESS_KEY|fly secrets|secrets import|export " services/field-ops/DEPLOY.md || true
echo
echo "== surrounding scripts/config files =="
fd -a '(\.env|fly\.toml|fly\.toml\.sample|.*env.*|.*deploy.*|.*deploy)' services/field-ops || trueRepository: alfieprojectsdev/movefaults Length of output: 6452 Do not guide operators to paste credentials into shell commands. Line 56 exports 🤖 Prompt for AI Agents |
||||||||||
| uv run alembic upgrade head # core schema, 13 revisions | ||||||||||
| uv run alembic -c services/field-ops/alembic.ini upgrade head # field_ops schema | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Verify before moving on — a partial migration surfaces as a confusing 500 later: | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| psql "$DATABASE_URL" -c "\dt field_ops.*" | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Expect 7 tables: `users`, `logsheets`, `staff`, `logsheet_observers`, | ||||||||||
| `equipment_inventory`, `logsheet_photos`, `equipment_history`. | ||||||||||
|
|
||||||||||
| ### Seed stations and staff **[you]** | ||||||||||
|
|
||||||||||
| The station list is what the observers pick from — it must contain the Palawan | ||||||||||
| sites they are actually visiting, which the 10 demo rows do **not**. | ||||||||||
|
|
||||||||||
| ```sql | ||||||||||
| INSERT INTO public.stations (station_code, name, municipality, province, monitoring_method, status) | ||||||||||
| VALUES ('XXXX', 'Site name', 'Municipality', 'Palawan', 'campaign', 'active'); | ||||||||||
|
|
||||||||||
| INSERT INTO field_ops.staff (full_name, initials, role, is_active) | ||||||||||
| VALUES ('Full Name', 'FN', 'field_staff', true); | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 2. Cloudflare R2 — photos **[you]** | ||||||||||
|
|
||||||||||
| 1. Cloudflare dashboard → R2 → **Create bucket**, e.g. `pogf-field-ops`. | ||||||||||
| Location hint: **APAC**. | ||||||||||
| 2. **Keep it private.** Site photos can show equipment, access routes and | ||||||||||
| occasionally people. Reads go through the API, not a public bucket URL. | ||||||||||
| 3. R2 → **Manage API Tokens** → Create token, permission **Object Read & Write**, | ||||||||||
| scoped to that one bucket. | ||||||||||
| 4. Note four values: account ID, access key ID, secret access key, bucket name. | ||||||||||
| The secret is shown **once**. | ||||||||||
|
|
||||||||||
| Free tier is 10 GB — roughly 3,300 phone photos at ~3 MB. A week in Palawan will | ||||||||||
| not come close. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 3. Backend container **[you]** | ||||||||||
|
|
||||||||||
| The existing `services/field-ops/Dockerfile` builds unmodified. Fly.io shown; | ||||||||||
| Railway and Render are equivalent. | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| cd services/field-ops | ||||||||||
| fly launch --no-deploy --name pogf-field-ops --region sin | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Set secrets — **names here, values from your password manager**: | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| fly secrets set \ | ||||||||||
| FIELD_OPS_PRODUCTION=1 \ | ||||||||||
| DATABASE_URL='...' \ | ||||||||||
| FIELD_OPS_JWT_SECRET="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" \ | ||||||||||
| FIELD_OPS_STORAGE_BACKEND=r2 \ | ||||||||||
| R2_ACCOUNT_ID='...' \ | ||||||||||
| R2_ACCESS_KEY_ID='...' \ | ||||||||||
| R2_SECRET_ACCESS_KEY='...' \ | ||||||||||
| R2_BUCKET='pogf-field-ops' | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| `FIELD_OPS_PRODUCTION=1` is the important one. It turns on a **fail-closed** | ||||||||||
| startup check that refuses to boot if the JWT secret is the shipped default or | ||||||||||
| under 32 characters, if storage is not R2, or if `DATABASE_URL` is missing. | ||||||||||
|
|
||||||||||
| That check exists because this repo is **public**. With the default secret, | ||||||||||
| anyone who reads it can mint a valid token for your URL and post logsheets as | ||||||||||
| any user. A service that boots happily in that state is worse than one that | ||||||||||
| refuses, because nobody finds out. | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| fly deploy | ||||||||||
| curl https://<your-app>.fly.dev/health # {"status":"ok",...} | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| If it will not start, read the logs — the refusal message names every missing | ||||||||||
| variable at once, and never prints a value. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 4. Frontend on Vercel **[you]** | ||||||||||
|
|
||||||||||
| 1. Import the repo. **Root directory: `services/field-ops/frontend`.** | ||||||||||
| 2. Edit `vercel.json` first — replace `REPLACE-WITH-BACKEND-HOST` with your | ||||||||||
| backend hostname: | ||||||||||
|
|
||||||||||
| ```json | ||||||||||
| { "source": "/api/:path*", "destination": "https://pogf-field-ops.fly.dev/api/:path*" } | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| The rewrite keeps the API same-origin, so there is no CORS preflight — one | ||||||||||
| fewer round trip on one bar of signal. If you deploy cross-origin instead, set | ||||||||||
| `VITE_API_BASE_URL` at build time and add the Vercel URL to | ||||||||||
| `FIELD_OPS_CORS_ORIGINS` on the backend. | ||||||||||
|
|
||||||||||
| 3. Deploy. Note the URL. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 5. Create the observer accounts **[you]** | ||||||||||
|
|
||||||||||
| One per person, so `submitted_by` records who filed each sheet. There is no | ||||||||||
| sign-up screen by design. | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| python3 -c "import bcrypt,secrets; pw=secrets.token_urlsafe(9); \ | ||||||||||
| print('password:', pw); print('hash:', bcrypt.hashpw(pw.encode(), bcrypt.gensalt()).decode())" | ||||||||||
|
Comment on lines
+169
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate DEPLOY.md and dependency/config files =="
fd -a 'DEPLOY\.md$|pyproject\.toml|uv\.lock|requirements.*|README.*' . | sed 's#^\./##' | head -200
echo
echo "== target lines =="
if [ -f services/field-ops/DEPLOY.md ]; then
nl -ba services/field-ops/DEPLOY.md | sed -n '150,180p'
fi
echo
echo "== relevant config files =="
for f in $(fd 'pyproject\.toml|uv\.lock|README.*' . | head -100); do
echo "--- $f"
sed -n '1,220p' "$f" | nl -ba | sed -n '1,220p'
done
echo
echo "== references to python3, uv run, bcrypt in deploy/docs =="
rg -n 'python3|uv run|uv|bcrypt|account|create|password|dependency' -S --glob '!node_modules' --glob '!dist' --glob '!build' .Repository: alfieprojectsdev/movefaults Length of output: 1081 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== target lines =="
awk 'NR>=150 && NR<=180 {printf "%5d\t%s\n", NR, $0}' services/field-ops/DEPLOY.md || sed -n '150,180p' services/field-ops/DEPLOY.md | awk '{printf "%5d\t%s\n", NR+149, $0}'
echo "== pyproject and lock files =="
for f in pyproject.toml uv.lock services/vadase-rt-monitor/uv.lock; do
[ -f "$f" ] || continue
echo "--- $f"
sed -n '1,260p' "$f"
done
echo "== project files =="
find . -maxdepth 3 \( -name 'pyproject.toml' -o -name 'uv.lock' \) -print | sort | while read -r f; do
echo "--- $f"
sed -n '1,220p' "$f"
done
echo "== references =="
rg -n 'python3|uv run|uv|bcrypt|accounts|account|password|dependency|install|create' -S --glob '!node_modules' --glob '!dist' --glob '!build' .Repository: alfieprojectsdev/movefaults Length of output: 50384 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== target lines =="
awk 'NR>=150 && NR<=180 {printf "%5d\t%s\n", NR, $0}' services/field-ops/DEPLOY.md
echo "== pyproject and uv.lock summaries (no rg across whole repo) =="
sed -n '1,220p' pyproject.toml
echo "--- uv.lock project packages around bcrypt/tools ---"
sed -n '1,260p' uv.lock | sed -n '/^name = /p;/^version = /p;/^source = /p;/^dependencies = /p;/^extra = /p;/optional = /' | head -200
echo "--- services/vadase-rt-monitor packages ---"
sed -n '1,220p' services/vadase-rt-monitor/pyproject.toml || true
sed -n '1,220p' services/vadase-rt-monitor/uv.lock || true
echo "== focused references in relevant docs and configs =="
rg -n 'python3|uv run|uv|bcrypt|accounts|account|password|dependency|install|create' -S pyproject.toml uv.lock services/field-ops/DEPLOY.md services/vadase-rt-monitor README.md || true
echo "== deterministic import-check behavior for clean host Python without bcrypt =="
python3 - <<'PY'
import importlib.util
spec = importlib.util.find_spec("bcrypt")
print("bcrypt_spec_found=", bool(spec))
PYRepository: alfieprojectsdev/movefaults Length of output: 5771 Run password generation with the project interpreter. This snippet imports Proposed command change-python3 -c "import bcrypt,secrets; pw=secrets.token_urlsafe(9); \
+uv run python -c "import bcrypt,secrets; pw=secrets.token_urlsafe(9); \📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||
| ``` | ||||||||||
|
|
||||||||||
| ```sql | ||||||||||
| INSERT INTO field_ops.users (username, hashed_password, role) | ||||||||||
| VALUES ('surname', '<hash>', 'field_staff'); | ||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Give each person their own password **through a private channel**, not a group | ||||||||||
| chat. Sessions last 8 hours — a full shift without re-entering anything. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 6. Verify before you send the URL | ||||||||||
|
|
||||||||||
| Do all of this on a real phone, not a desktop browser. **Step 4 is the one that | ||||||||||
| matters most and the one most likely to be skipped.** | ||||||||||
|
|
||||||||||
| - [ ] Open the URL, sign in with a real observer account | ||||||||||
| - [ ] Station dropdown lists the **Palawan** sites, not the demo ten | ||||||||||
| - [ ] "Add to Home Screen" installs it; it opens without browser chrome | ||||||||||
| - [ ] **Enable airplane mode.** Fill a full campaign sheet, attach a photo, | ||||||||||
| submit. Expect *"Saved offline — including the photo."* Check the Queue | ||||||||||
| tab shows it as **photo held** | ||||||||||
| - [ ] Turn signal back on. The queue should drain by itself; the record moves to | ||||||||||
| synced | ||||||||||
| - [ ] Confirm in the DB that the row exists **and** `logsheet_photos` has a row | ||||||||||
| with an `r2://` path | ||||||||||
| - [ ] Open the object in the R2 dashboard and confirm it is the right image | ||||||||||
| - [ ] Force-quit the app between queueing and syncing, reopen — the queued sheet | ||||||||||
| must still be there | ||||||||||
|
|
||||||||||
| If step 4 fails, stop and fix it before anyone travels. That is the path they | ||||||||||
| will use most, and its failure mode is silent. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 7. Brief the staff on three things | ||||||||||
|
|
||||||||||
| 1. **Sign in once while you still have signal.** The app then works offline for | ||||||||||
| the rest of the day, but the first login needs a connection. | ||||||||||
| 2. **Do not clear browsing data, and do not use private/incognito.** Queued | ||||||||||
| sheets live in the browser's storage; clearing it destroys unsynced work. | ||||||||||
| 3. **Check the Queue tab before leaving a site.** If it shows pending items, | ||||||||||
| that is normal — but the count should drop to zero once back in signal. | ||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 8. Known gaps — decide before, not during | ||||||||||
|
|
||||||||||
| - **No frontend tests.** The slant→RH computation is the only real domain | ||||||||||
| arithmetic in the UI and has none. A wrong answer there silently corrupts the | ||||||||||
| vertical component of every campaign occupation. It is verified by hand for | ||||||||||
| `TRM55971-00` only. | ||||||||||
|
Comment on lines
+220
to
+223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 4 'slant|TRM55971|relative.?humidity|\bRH\b' services/field-ops/frontend/src || true
rg -n -C 3 'vitest|jest|playwright|test:' \
services/field-ops/frontend/package.json services/field-ops/frontend || trueRepository: alfieprojectsdev/movefaults Length of output: 17354 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== LogSheetForm slice =="
sed -n '1,80p' services/field-ops/frontend/src/components/LogSheetForm.tsx
sed -n '160,235p' services/field-ops/frontend/src/components/LogSheetForm.tsx
echo
echo "== frontend package and test files =="
fd -a '^(package\.json|vitest\.config\..*|jest\.config\..*|playwright\.config\..*|setup.*\.(js|ts|mjs|cjs))$' services/field-ops/frontend | sed 's#^\./##' | sort
sed -n '1,180p' services/field-ops/frontend/package.json 2>/dev/null || true
rg -n -C 2 'describe|it\s*\(|test\s*\(|expect\(|const .*to\s*be|assert' services/field-ops/frontend/src services/field-ops/frontend 2>/dev/null || true
echo
echo "== deploy lines around test coverage note =="
sed -n '200,235p' services/field-ops/DEPLOY.md
echo
echo "== static search for calculateHelper / antenna math in tests and src =="
rg -n 'calculate|antenna|rinex_height|avg_slant|slantN|VO|Math\.sqrt|rhValue' services/field-ops/frontend/src services/field-ops/frontend -g '!node_modules' || trueRepository: alfieprojectsdev/movefaults Length of output: 21260 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== submit validation slice =="
sed -n '235,290p' services/field-ops/frontend/src/components/LogSheetForm.tsx
echo
echo "== LogSheetIn schema in api =="
sed -n '35,70p' services/field-ops/frontend/src/services/api.ts
echo
echo "== JS numeric/NaN behavior used by current slant parser =="
node - <<'JS'
const samples = [
["0", "0", "0", "0"],
["0", "0", "0", "0.1698"],
["0", "0", "0", "0.1699"],
["1", "1", "1", "1"],
["0", "", "0", "0"],
["0", "+.", "0", "0"],
["0", "-", "0", "0"],
["0", "1.4320", "1.4318", "1.4322"],
];
for (const [N,E,S,W] of samples) {
const Nf = parseFloat(N), Ef = parseFloat(E), Sf = parseFloat(S), Wf = parseFloat(W);
const allSlantsFilled = Nf === Nf && Ef === Ef && Sf === Sf && Wf === Wf;
const avgSH = allSlantsFilled ? (Nf + Ef + Sf + Wf) / 4 : undefined;
const C = 0.1698, VO = 0.0444;
const rh = avgSH !== undefined ? Math.sqrt(avgSH * avgSH - C * C) - VO : undefined;
console.log(JSON.stringify({N,E,S,W,Nf,Ef,Sf,Wf,allSlantsFilled,avgSH,rh}));
}
JSRepository: alfieprojectsdev/movefaults Length of output: 4447 Add automated coverage for the slant-to-RH calculation before field deployment. The campaign submit path computes and submits 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||
| - **No password reset.** Locked out means you issue a new hash by hand. For a | ||||||||||
| one-week trial that is acceptable; make sure someone with DB access is | ||||||||||
| reachable. | ||||||||||
| - **No admin view.** Reading the data means SQL against Neon. | ||||||||||
| - **Free tiers sleep.** A Neon or Fly instance idle for hours takes a few | ||||||||||
| seconds on first request. Harmless, but tell staff so a slow first load is not | ||||||||||
| read as a failure. | ||||||||||
| - **Photos are never deleted.** No retention policy; at ~3 MB each the 10 GB | ||||||||||
| free tier is fine for this trip and will need a decision later. | ||||||||||
|
Comment on lines
+231
to
+232
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Resolve photo retention and archive protection before deployment. Lines 231-232 state that photos are never deleted. Lines 240-243 export the database and photos to local paths without an explicit encryption, access-control, or cleanup step. Lines 89-90 state that photos can show people. Define a retention period and deletion procedure for both R2 objects and Also applies to: 240-243 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| --- | ||||||||||
|
|
||||||||||
| ## 9. After the fieldwork | ||||||||||
|
|
||||||||||
| Export before tearing anything down: | ||||||||||
|
|
||||||||||
| ```bash | ||||||||||
| pg_dump "$DATABASE_URL" --schema=field_ops --schema=public -Fc -f palawan-$(date +%Y%m%d).dump | ||||||||||
| rclone sync r2:pogf-field-ops ./palawan-photos/ # or the R2 dashboard | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Locate file =="
git ls-files | rg '(^|/)DEPLOY\.md$' || true
echo "== Inspect relevant DEPLOY section =="
if [ -f services/field-ops/DEPLOY.md ]; then
sed -n '210,260p' services/field-ops/DEPLOY.md
fi
echo "== Search for rclone commands in DEPLOY.md =="
rg -n "rclone\s+(sync|copy)\b|palawan-photos|r2:pogf-field-ops" services/field-ops/DEPLOY.md || true
echo "== Check surrounding section headings/context =="
rg -n "^(#{1,6} .+)$|palawan|photos|R2|rclone" services/field-ops/DEPLOY.md | head -200Repository: alfieprojectsdev/movefaults Length of output: 3772 🌐 Web query:
💡 Result: Yes, the rclone sync command is explicitly designed to make the destination match the source exactly, which includes deleting any files in the destination that are not present in the source [1][2][3]. Key details regarding this behavior include: - Purpose: The sync command modifies the destination only to ensure it is identical to the source [1][4]. - Deletion Behavior: If a file exists in the destination but not in the source, rclone will delete it from the destination [1][2]. - Prevention: If you do not want to delete files from the destination, you should use the rclone copy command instead, which copies files to the destination without deleting any existing files [1][2]. - Safety Mechanisms: Because this command can lead to data loss, it is highly recommended to use the --dry-run flag first to preview the changes that will be made [4][5]. Additionally, you can use the --backup-dir flag to move files that would have been deleted into a separate backup directory, or use --max-delete to limit the number of files that can be deleted in a single operation [1][2][5]. If you encounter errors during the process, rclone will typically not delete files in the destination to avoid unintended data loss [1][2]. Additionally, files that are excluded via filter rules are not deleted unless the --delete-excluded flag is specifically used [1][2]. Citations:
Use a non-destructive archive copy.
Safer command-rclone sync r2:pogf-field-ops ./palawan-photos/ # or the R2 dashboard
+rclone copy r2:pogf-field-ops "./palawan-photos-$(date +%Y%m%d-%H%M%S)/" # or the R2 dashboard📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| ``` | ||||||||||
|
|
||||||||||
| Then follow the project's own rule: the archive needs a `sha256sum` manifest | ||||||||||
| committed to git, not just the files. Fingerprints stored only beside the data | ||||||||||
| prove nothing if that disk is what failed. | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language to the architecture diagram fence.
Line 6 opens a fenced code block without a language. This triggers markdownlint MD040. Change the opener to
```text.Proposed fix
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 6-6: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Source: Linters/SAST tools