Skip to content

feat: record last_used when a project is opened - #412

Open
NKeleher wants to merge 1 commit into
mainfrom
feat/record-project-activation
Open

NKeleher wants to merge 1 commit into
mainfrom
feat/record-project-activation

Conversation

@NKeleher

@NKeleher NKeleher commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Summary 🚀

What does this PR do? 📝

Opening a project from the home page now stamps last_used for that project
in cache/projects.json. A new helper, start_view._record_last_used, is
called from _activate_project. If the registry can't be written, the
failure is logged and the project still opens.

Why is this change needed? 🤔

  • For every user: today last_used is only written when a project is
    created, so the home page's "Last used" caption and its "Last used" sort
    show creation time, not when the project was last opened.
  • For hosted deployments: DataSure Cloud hosts the unmodified datasure package from PyPI for IPA's multi-user pilot: one Linux container per signed-in user, with project state synced to Azure Blob Storage and a per-project write lock so only one person edits a project at a time. It's one of three small, independent PRs that let the hosting layer stop working around DataSure (the others: read-only notice feat: show a read-only notice when the host marks a project read-only #413, headless keyring guidance feat: explain unusable keyring backends on headless servers #414). _activate_project only sets st.session_state.st_project_id,
    which leaves nothing on disk to show which project is open. DataSure Cloud
    therefore holds the write lock on every project a user can edit for their
    whole session, which locks colleagues out of projects nobody is working on.
    With this change the lock can follow the project that is actually open.

How was this implemented? 🛠️

  • _record_last_used(project_id) loads the registry with the existing
    load_projects, updates last_used with _now_str(), and saves with
    _save_projects. Unknown project ids are left untouched.
  • OSError and ValueError (for example a read-only cache or a corrupt
    projects.json) are logged as a warning, so opening a project never fails
    because of the registry.
  • Docs: docs/ARCHITECTURE.md (what projects.json holds), CHANGELOG.md
    under [Unreleased].

How to test or reproduce ? 🧪

  • uv run python -m pytest tests/views/test_start_view.py. New
    TestRecordLastUsed: activation updates last_used and keeps
    created_at; an unknown id leaves the registry unchanged; an unwritable
    registry still opens the project.
  • Manual: create two projects, open the older one, then return home. It
    shows a fresh "Last used" and moves to the top of the "Last used" sort.
  • Full suite: just test (4580 passed, 6 skipped); just pre-commit-run
    clean.

Screenshots (if applicable) 📷

N/A

Checklist ✅

  • I have run and tested my changes locally
  • I have limit this PR to less than 1000 lines of code change (if not, explain why)
  • I have updated/added tests to cover my changes (if applicable)
  • I have updated/added requirements to cover my changes (if applicable): N/A, no new dependencies
  • I have run linting and formatting on any code changes (if applicable)
  • I have updated the documentation (README, etc.) accordingly
  • I have reviewed and resolved any merge conflict

Reviewer Emoji Legend

:code: Meaning
😃👍💯 :smiley: :+1: :100: I like this...

...and I want the author to know it! This is a way to highlight positive parts of a code review.
⭐⭐⭐ :star: :star: :star: Important to fix before PR can be approved...

And I am providing reasons why it needs to be addressed as well as suggested improvements.
⭐⭐ :star: :star: Important to fix but non-blocking for PR approval...

And I am providing suggestions where it could be improved either in this PR or later.
⭐ :star: Give this some thought but non-blocking for PR approval...

...and consider this a suggestion, not a requirement.
❓ :question: I have a question.

This should be a fully formed question with sufficient information and context that requires a response.
📝 :memo: This is an explanatory note, fun fact, or relevant commentary that does not require any action.
⛏ :pick: This is a nitpick.

This does not require any changes and is often better left unsaid. This may include stylistic, formatting, or organization suggestions and should likely be prevented/enforced by linting if they really matter
♻️ :recycle: Suggestion for refactoring.

Should include enough context to be actionable and not be considered a nitpick.

_activate_project now stamps last_used in cache/projects.json, so the home
page's "Last used" caption and sort reflect opens rather than only
creation, and tools managing the cache can tell which project is open. A
registry that can't be written is logged and does not block opening.
@sonarqubecloud

Copy link
Copy Markdown

@claude claude 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.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines +188 to +195
try:
projects = load_projects()
if project_id not in projects:
return
projects[project_id]["last_used"] = _now_str()
_save_projects(projects)
except (OSError, ValueError):
logger.warning("Could not record last_used for project %s", project_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Opening a project can now wipe the project registry, leaving the home page unable to load. Before this change, opening a project never wrote cache/projects.json. _record_last_used rewrites the file on every open through _save_projects (start_view.py:118-122), which truncates it with open(..., "w") before writing. If json.dump fails partway, the warning at line 195 swallows the error and the file is left empty or partly written. Fix: make _save_projects write atomically: dump to a temp file in the same directory, then os.replace it over the registry. That covers this path and the existing create and delete writers.

Why this was flagged

Trigger: a user opens any project from the home page, calling _activate_project (start_view.py:198), which now calls _record_last_used (line 201) and then _save_projects (line 193). _save_projects (lines 120-122) opens projects.json with mode "w", so the file is truncated before json.dump writes anything. If the write then fails with OSError (disk full or quota on the cache volume, or an I/O error on a synced or mounted cache), the except (OSError, ValueError) at line 194 logs a warning and the project still opens. The registry is left empty or holding truncated JSON. On the next home-page render, load_projects() at line 593 calls json.load with no guard, so json.JSONDecodeError crashes the page, or every project disappears from the list. On the base branch, opening a project only set st.session_state.st_project_id and never wrote the registry, so this exposure was limited to the rare create and delete actions. The new catch-and-continue hides the damage until the next home-page load.

Verification: _activate_project (start_view.py:201) now calls _record_last_used on every open, and _save_projects (lines 118-122) truncates with open(projects_file, "w"). The except at lines 194-195 only logs a warning, leaving cache/projects.json empty or partly written. load_projects() at line 593 then raises JSONDecodeError. Before this change, opening a project never wrote the registry.

Comment on lines +189 to +193
projects = load_projects()
if project_id not in projects:
return
projects[project_id]["last_used"] = _now_str()
_save_projects(projects)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) Two sessions sharing one cache can now lose a newly created or deleted project when the other session opens a project. _record_last_used reads the full registry, changes one entry and writes the whole file back, with no lock. Another session can save or delete a project between that read and that write. Fix: make the read and write one locked step (for example a file lock around load and save), or update only the one entry under that lock. An atomic file replace alone does not prevent this lost update.

Why this was flagged

Trigger: two Streamlit sessions (browser tabs) share one cache/ directory, as in a hosted per-user container with several tabs open. Tab A opens project Y. _record_last_used calls load_projects() (start_view.py:189) and gets a snapshot without project X. Meanwhile tab B's save_project (start_view.py:150-157) writes X into the registry. Tab A then calls _save_projects(projects) (start_view.py:193) with its old snapshot, which overwrites the file and drops X. X's directory still exists on disk, but it no longer shows on the home page. A deletion in tab B can come back the same way. On the base branch, opening a project did not write the registry, so this race only existed between two create or delete calls. Now every project open is a writer. Nothing in the module locks the registry.

Verification: _record_last_used (start_view.py:188-193) calls projects = load_projects(), changes one entry and then calls _save_projects(projects). That function (lines 118-122) truncates the file and dumps the whole dict, with no lock and no re-read. The new project's directory exists but the registry no longer lists it, so it disappears from the home page.

This branch has not been deployed

No deployments
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