Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Record the unported attachment thumbnails/click-to-view as a rewrite task - #121

Closed
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap
Closed

Record the unported attachment thumbnails/click-to-view as a rewrite task#121
corrin wants to merge 19 commits into
mainfrom
docs-attachment-view-gap

Conversation

@corrin

Copy link
Copy Markdown
Owner

Prod bug report 2026-08-31 after the flip: attachment "view" is gone. v1's attachments tab rendered a clickable thumbnail per file; v2's JobAttachmentsTab.tsx renders only download/delete icons and nothing in frontend/src/ calls the ported getJobFileThumbnail endpoint. This records the porting gap as a DEFERRED task in docs/rewrite-status.md (the download 404 is a separate ops issue on the prod host, diagnosed in-session, no code change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS

corrinand others added 16 commits August 31, 2026 06:23
…t-password surface
Django's four standard validators, with the similarity attributes named
explicitly because Staff has no username/email attribute for the defaults
to find. _set_staff_password was already wired to enforce them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4YMRsFzJQrHHzBkXHfYFC
The v1 repo now sits at ../docketworks_v1, so gen_v1_operations.py's
V1_REPO and every live doc pointing at ../docketworks (which now
resolves to this repo itself) move with it. initial_install.md clones
the renamed GitHub repo. DB names stay docketworks_v2 - the database
was not renamed. Historical records (cutover checklist, plan docs)
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0164MHjth7CvHoYQDEcZHahA
Verifies the current password (400 on mismatch, ADR 0038 transparent
post-auth), routes the new value through _set_staff_password so the
validators judge it and password_needs_reset clears.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
While the flag is set, CookieJWTAuth refuses every path outside the /me/
and /me/password/ allowlist with code "password_change_required"
(error_id null, no AppError row — expected security outcome, ADR 0013).
A frontend redirect the API does not back would leave every endpoint
serving a session whose credential we have decided not to trust.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…rfaces
StaffCreateIn/StaffUpdateIn carry the "must change at next login" flag;
an explicit flag outlives _set_staff_password's clear, so an admin can
issue a known temporary password already flagged. UserProfile and
StaffListItemOut expose the stored value — the route guard reads /me/,
and the edit modal must render the real state or clear it by accident.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…ptor
Login response and /me both carry password_needs_reset; the login page
and the authed layout route flagged sessions to /change-password (its
own top-level route — nesting under _authed would loop that guard). The
transport interceptor hard-navigates on the auth layer's typed 403 so a
session flagged mid-flight gets walked to the exit too; the server gate
stays the control, all of this is navigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The password-reset email gives application code its first Google call,
and apps must never import from scripts — so the one credential builder
moves to apps/core/gauth.py (scripts/gdocs re-imports it) and
apps/core/gmail.py becomes the one application email sender: plain-text
send as the instance's Workspace user via domain-wide delegation with
the gmail.send scope, proven by the 2026-08-31 delegation probe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
POST /password-reset/ answers a fixed 200 whether or not the address has
an active account, and emails a uid+token link via the delegated Gmail
sender. POST /password-reset/confirm/ exchanges the link for a new
password through _set_staff_password (validators run, flag clears);
refusals are declared 400 bodies because the envelope masks anonymous
exception text and the validator's reason is the response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
/forgot-password always lands on the same sent-confirmation copy — the
server's fixed 200 must not be undone by a chattier client. The emailed
link lands on /reset-password, whose uid/token search params normalise
to the invalid-link state rather than crashing; a dead link surfaces as
the server's 400 detail on submit, since the token is deliberately
unverifiable without attempting the change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
The spec locks a flagged login to /change-password until a strong
password lands, and walks the anonymous forgot/reset paths — the request
step submits an account-less address (the fixed 200 sends nothing), so
the E2E stack needs no Gmail configuration. The real delegated send is
the integration gate's job (ADR 0050), addressed to the delegated
subject itself so the probe stays in the instance's own inbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…t falls
Playwright parses a two-element test.use() array whose second entry is
an object (a RegExp qualifies) as a [value, options] fixture tuple, so
the console filter received a bare string and threw after every step
had passed; one combined 400|401 pattern is unambiguous. Both specs are
green under run_e2e.sh (12 passed), which is what deletes the
weak-password bullet from rewrite-status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
… dialog un-flag bug
Adversarial-review batch on the branch diff.
Every issued token now carries a fingerprint of the password hash
(issue_refresh_token is the one mint; stubs/ninja_jwt grows Token.get),
checked at authentication and refresh — a change or reset evicts the
attacker who knew the old password and holds cookies, this slice's own
threat model. The change endpoint re-mints the caller's cookies so
changing your own password keeps you signed in; claimless tokens are
refused by the same comparison, never grandfathered (ADR 0017).
The reset request matches either email column exactly as login does
(payroll-only staff could otherwise never reset, silently) and queues
the Gmail send — synchronous sending ran only for matched addresses,
making latency and a Gmail outage's 500 an account-existence oracle.
SSE streams render the typed 403 instead of 500-looping a flagged
EventSource.
Frontend: a patch carrying a password now always carries the checkbox
state — the dirty-only diff silently un-flagged an already-flagged
account on a temp-password reissue; the auth screens share one
AuthCard/PasswordField/FormAlert instead of three drifting copies, and
the typed-403 predicate lives with its siblings in error-message.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…link host
The high-effort review pass on the full branch plus CodeRabbit, verified
finding by finding.
The dev/E2E seed no longer flags its staff (the flag now locks sessions,
and those logins exist to act AS someone with the printed password);
flag_weak_passwords documents that it flags EVERYONE and confines live
sessions immediately. Re-entering the current password no longer
satisfies a forced change. The change endpoint's refusals are declared
400s (PasswordErrorOut, shared with confirm) so the wire contract
carries their shape and no AppError row records an expected refusal.
The reset link pins its host to settings.APP_DOMAIN — ALLOWED_HOSTS
accepts localhost and USE_X_FORWARDED_HOST let an anonymous caller
poison a victim's genuine reset email with a dead link.
One definition each for concepts that had grown twins: the login-email
match (Staff.objects.sole_login_match, shared by the login backend and
the reset request), the fingerprint comparison, and the typed-403 body.
The stale-fingerprint refresh no longer clears cookies (a racing 401
could delete the session the changer was just re-minted). /reset-password
joins route_reachability's entry list (reached only by the emailed
link); the forced change screen gains sign-out and forgot-password
exits; the deep link that started a flagged session survives through
the forced change. The fleet-wide one-time re-login at deploy is
recorded in rewrite-history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
CI failed the outbox assertions with the request logging QUEUED and a
200: on the runner, .delay() published to the live redis service
instead of running inline, while the identical invocation runs eagerly
on a dev box. The repo already knew better — eager .delay() is "a
property of the test settings, not of the product"
(test_job_files_api.py) — so the endpoint tests now capture .delay's
arguments (the queued job IS what the endpoint owes), and the task's
own contract (recipient, subject, link, body) gets a direct-call test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
Written when it was "v2's only Google client" (its own comment), the
command carried a private --credentials path and its own delegation
checks. That premise ended when the Gmail sender landed: credentials
now come from apps/core/gauth.py like every other Google entry point
(GCP_CREDENTIALS key file, delegated subject with its fail-loud
resolution), leaving from_service_account_file with exactly one call
site in apps/. The Drive client construction stays in the command —
clients are domain-owned the way gmail.py builds its own; credentials
are what must never fork (ADR 0039).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWTJcCbpQdzWLMVNg1QaLb
…026-08-31)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@coderabbitai

coderabbitaiBot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcff32ad-75a3-4a65-9747-c78683d8db28


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

corrinand others added 3 commits August 31, 2026 19:16
The 2026-08-31 incident class: DROPBOX_WORKFLOW_FOLDER pointed one
directory above the Job-* tree, so every attachment 404d and new job
folders spawned at the Dropbox top level while every daemon reported
healthy. Three legs: reconfigure reads the operator's value back instead
of reverting it to the empty instance dir, verify-instance.sh gates on
every JobFile row resolving on disk, and check_jobfiles names the
wrong-root cause first when nothing resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
dw-run.sh sources the instance .env, so an unquoted space-containing
workflow path (the real MSM value) aborts the source — the template now
renders the value quoted, which read_env_value already strips on the
reconfigure read-back. check_jobfiles resolves through
job_file_full_path so a row the endpoint refuses cannot pass the check,
and counts root-escaping rows as failures (the try/except movement in
code-quality.md is this catch). Comment adjacency in verify-instance.sh
restored, rationale comments carry their Fable: provenance, and the
template's .env.example sync rule is honoured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbe63GrSY11WxJm6t2T5sS
@corrin

Copy link
Copy Markdown
OwnerAuthor

Replaced by #122: this branch predated the #120 squash, so the PR diff rendered the entire merged password workstream alongside the real 9-file slice. #122 carries the same commits re-parented onto main.

@corrincorrin closed this Aug 31, 2026
@corrin
corrin deleted the docs-attachment-view-gap branch August 31, 2026 07:33
Sign up for freeto 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

@corrin