Skip to content

fix: stop silent data loss in zones, API keys, CSV and triggers - #314

Merged
MrYuion merged 10 commits into
developfrom
fix/data-loss
Sep 30, 2026
Merged

MrYuion merged 10 commits into
developfrom
fix/data-loss

Conversation

@MrYuion

@MrYuion MrYuion commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Several flows lost or corrupted data without an error:

  • The zone form sent parent_id: '' if you saved before the parent zone loaded, or if loading it failed. The zone lost its building or level.
  • Zone and group parent pickers let you pick a descendant, which makes a loop.
  • Clicking two items fast could leave the first one active while the URL showed the second. Edit and delete then acted on the wrong item.
  • API key expiry added a TTL in seconds to Date.now() in milliseconds. A 1 day key expired after about 86 seconds. A key with only an expiry date got NaN.
  • parseCSV split on newlines before it read quotes. CRLF files lost their last column, and quoted newlines split rows. Zone tree export then import corrupted descriptions. Export also dropped columns that the first row did not have.
  • Trigger action and condition deletes used findIndex. When it returned -1, splice(-1, 1) removed the last entry.
  • Other smaller cases: extensions matched by name only, the edge API key banner cleared on cancel, deleted edges came back, settings added during a save were lost, and an empty domain settings editor threw.

This is part of a stack of 6 PRs from a review of the whole app. Merge them in order: confirm-cancel, security, data loss, broken features, async errors, cleanup. Each PR targets the branch before it, so the diff shows only its own changes.

Fix

  • The zone form keeps the stored parent_id unless the user changes the picker.
  • common/hierarchy.ts filters descendants out of parent pickers. The group picker also hides groups from other domains, refreshes when the domain changes and clears the parent.
  • setItem drops responses from older requests.
  • apiKeyExpiry() works in seconds. It has tests.
  • parseCSV reads one character at a time and handles quotes and CRLF. jsonToCsv uses the given field list for the header.
  • Trigger edits check the index and change copies. The action modal's is_new was inverted.
  • Existing OAuth apps show their stored client ID. New apps show md5 of the lowercased redirect URI, which is what the backend creates.
  • Fix the smaller cases listed above.

cleanObject is not changed. The backend does not handle empty values.

Checks

  • tsc, bun run lint, bunx vitest run (1008 tests) pass. New tests cover the CSV parser (CRLF, quoted newlines, round trip), API key expiry and the hierarchy filter.
  • All 6 branches were merged and tested in a browser against a local PlaceOS stack (nightly images, with core and triggers). Test agents created their own records, checked results through the API, and removed the records after. A zone saved while its parent GET was delayed or returned 404 kept its parent_id. A 1 day key stored now + 86396 s. A description with a comma and a newline survived export and import.

Made by Claude Opus 5.5 (1M context) in Claude Code (T3 Code).

🤖 Generated with Claude Code

MrYuion and others added 10 commits September 30, 2026 13:11
The parent picker effect wrote `parent_id: ''` as soon as the form opened,
because the picker starts empty until `showZone` resolves. Saving early, or
a failed `showZone`, detached the zone from its parent. The effect now only
syncs `parent_id` after the user changes the picker, and a failed parent
lookup is caught.

Zone and group parent pickers now hide the item's descendants, so a save
can no longer create a cycle. The group picker also hides groups from other
authorities.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- setItem now tracks the latest request, so fast A to B navigation can no
  longer leave A active while the URL shows B.
- The duplicate modal handler no longer clears the active item when every
  duplicate fails.
- Settings changed while a save is in flight stay pending, and a failed save
  puts its settings back into the pending list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- API key expiry added `ttl` seconds to a millisecond timestamp, and gave
  NaN when only `expires_at` was set.
- Cancelling the edge modal wiped the one-time API key banner.
- Deleting a second edge or build job brought the first one back.
- Editing or removing an extension removed every extension with the same
  name in other types. Saves now also report errors instead of hanging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
parseCSV split on `\n` before it read quotes. CRLF files kept `\r` in the
last header, so that column was lost, and quoted newlines split rows. Since
jsonToCsv writes quoted newlines, a zone tree export did not import back
correctly. The parser now reads one character at a time and tracks quotes
across lines.

A failed zone tree import now reports how many zones it created.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The "preserve client ID" toggle ignored the stored value, and when on it
  hashed the original redirect URI instead of keeping the stored uid.
- An empty config or internals editor passed validation but threw on save
  with no message. Empty now means `{}`, errors show a toast, and a saving
  state stops double saves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
findIndex by JSON can return -1, and splice(-1) then removed or replaced
the last entry. Missing entries now show an error. The action modal also
works on copies, so a failed save leaves the trigger unchanged, and
`is_new` is no longer inverted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jsonToCsv built the header from the first row only. The zone tree export
dropped display_name, code or location for every row when the first zone
did not have them. Now it uses the given field list, or the union of keys
across all rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The backend ignores a uid from the client. It sets md5(lowercase
redirect_uri) on create and keeps it on update. The form showed a new md5
for existing apps when preserve was off. Now existing apps show the stored
uid, and new apps show the md5 the backend will make.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The parent search kept the old authority's groups until the search text
changed, and a parent from the old authority stayed selected. The search
field now queries again when its query function changes, and the group
form clears the parent on authority change.

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

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
backoffice Ready Ready Preview Sep 30, 2026 3:23am UTC

Base automatically changed from fix/security-hardening to develop September 30, 2026 04:45
@MrYuion
MrYuion merged commit a2b212a into develop Sep 30, 2026
4 of 5 checks passed
@MrYuion
MrYuion deleted the fix/data-loss branch September 30, 2026 04:49

This branch was successfully deployed

1 active deployment
Preview — 78f73aef Deployed Sep 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant