Make file shares a first-class source type in the console (#196) - #204
Merged
icebergai-review-bot[bot] merged 2 commits intoAug 20, 2026
Merged
Conversation
The API has supported `fileshare` since #145, but the console's form had fields for Confluence and Jira only, so a share could be configured through the API and nowhere else. #187 stopped that from 500ing; a graceful refusal is not support. The share's own fields are now on the form — protocol, mount path, roots, include/exclude globs, symlink policy and the per-file ceiling — and `fileshare` joins the type select, which a test now holds against the API's own `SUPPORTED_SOURCE_TYPES` rather than a list repeated in the test. It is deliberately not the Confluence form with different labels. A share has no base URL, no Cloud-vs-DC choice, and no credential: the mount carries the host, the share name and the authentication, so those fields are hidden for this type and the form says why rather than leaving a gap where the token box is on every other type. `_connection_form` returns early for it, because `FileshareConnection` forbids extras and would reject a blob carrying `base_url` or `include_comments` — which every save posts, since all three scope blocks stay in the DOM. Three consequential details: * `base_url` and `credential` become optional form parameters. The browser posts neither for a share, and a missing required `Form()` is FastAPI's own 422 — an error page rather than the form the analyst is looking at. * `max_file_bytes` is read as text and converted here for the same reason: a mistyped ceiling re-renders the form with a message. * The connectivity-test button is not offered where no probe exists. `probe_source` already refused a share and said why; the console showed a button that could only ever produce that refusal. `PROBEABLE_TYPES` is the API's own answer to "is there a check for this type", asked before offering. The chip helpers do not upper-case: roots and globs are paths, where case is significant, unlike a space or project key. A leading slash is trimmed, since a root is relative to the mount and the API answers 422 for one. Two tests that pinned #187's refusal are superseded — one by the new "editing a share saves it", the other rewritten to cover the failure that is still reachable: a hand-posted body that does not match its type. Refs #145. Claude-Session: https://claude.ai/code/session_012sohE85sRDt6t2w3936rGJ Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Completed bounded review across 1 immutable scope(s). 1 high-severity correctness finding.
Scope health
Convergence: healthy. Review mode: initial.
Recommended action: CONTINUE_INCREMENTAL.
- No escalation signals.
Prior findings
| Finding | Status |
|---|---|
| — | No prior finding state |
New findings
Root cause: The Alpine state omits the server-provided max-file-size value.
- BLOCKER · high: Fileshare forms discard the displayed maximum-file-size default —
apps/api/src/iceberg_api/web/static/js/tags.js:165
Status: NEW. Attribution: new_in_scope.
Alpine initializesmaxFileBytesto an empty string and never hydrates it from the existing connection. Because the input is bound withx-model, Alpine replaces the server-renderedvalue="33554432"(or the saved value) with that empty model value. Submitting an untouched create or edit form therefore sends an emptymax_file_bytes, which_fileshare_connectionconverts to0.
Invariant: An untouched edit form must preserve the source configuration, and the displayed default must be the value submitted.
Ownership: Fileshare source web form state and submission assembly.. Behaviour: Creating or editing a fileshare source from the console without manually re-entering the maximum file size..
Evidence:tags.jsdefinesmaxFileBytes: ''andinit()hydrates roots/include/exclude but notmaxFileBytes; the fileshare input usesx-model="maxFileBytes". The route converts blank input withint(fields.max_file_bytes.strip() or 0). Tests postmax_file_bytesexplicitly, so they do not exercise the rendered form behavior.
Independent assessment: Confirmed: the fileshare input binds x-model="maxFileBytes", while the component initializes it to '' and init() never loads connection.max_file_bytes. Alpine therefore writes the empty model value to the field; submission converts it to 0, and FileshareConnection requires max_file_bytes >= 1, so untouched fileshare create/edit saves fail validation.
Fix-induced regressions
- None evidenced.
Uncertainty
- No material uncertainty recorded.
Validation
- Exact-head CI passed per supplied review data; reviewed the supplied immutable diff.
Residual risks
- None identified.
`x-model` owns that input and writes the model value into it on init, so the server-rendered `value="33554432"` was replaced with the component's empty default. An untouched create or edit therefore posted a blank ceiling, which became `0`, which `FileshareConnection` rejects (`ge=1`) — so saving a share without retyping the size failed. The tests posted the field explicitly and never saw it. The value now travels through the JSON island like every other piece of server state on this form, the element carries no `value=` for the model to overwrite, and the default is named once in `sources.schemas` rather than repeated as a literal in the template. A blank ceiling is also now refused in the route with a sentence, instead of being read as zero and earning a schema message about a field the analyst did not think they were setting. Two tests: the island carries the default on a new form and the stored value on an edit, and a cleared field is refused. The `tags.js` hydration itself has no test — there is no JS harness in this repo — but it cannot silently regress without the island assertion failing first. Claude-Session: https://claude.ai/code/session_012sohE85sRDt6t2w3936rGJ Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict
APPROVE
Completed bounded review across 1 immutable scope(s). No remaining findings. The prior fileshare form-state defect is fixed by hydrating Alpine with the persisted/default ceiling and rejecting an explicitly cleared value.
Scope health
Convergence: healthy. Review mode: delta.
Recommended action: CONTINUE_INCREMENTAL.
- No escalation signals.
Prior findings
| Finding | Status |
|---|---|
| F-09C5C55BFC07 | Resolved |
New findings
No new findings.
Fix-induced regressions
- None evidenced.
Uncertainty
- No material uncertainty recorded.
Validation
- Prior blocker F-09C5C55BFC07 resolved: sources.py adds maxFileBytes from connection.max_file_bytes or DEFAULT_MAX_FILE_BYTES; tags.js hydrates this.maxFileBytes; regression tests cover new-form default, edit-form persistence, and blank-value rejection.
- Reviewed the supplied immutable follow-up diff and re-evaluated the prior blocker.
Residual risks
- None identified.
icebergai-review-bot
Bot
deleted the
claude/codebase-review-cleanup-ovlli4
branch
August 20, 2026 01:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #196.
The API has supported
filesharesince #145, but the console's form had fields for Confluence and Jira only — so one whole connector was API-only to configure, which contradicts the console's "just another client of the same routes" positioning. #187 stopped that from 500ing; a graceful refusal is not support.What it is not
It is deliberately not the Confluence form with different labels. A share has:
_connection_formreturns early for the type, becauseFileshareConnectionforbids extras and would reject a blob carryingbase_urlorinclude_comments— which every save posts, since all three scope blocks stay in the DOM so switching type does not discard typing.Three details that decide whether it works
base_urlandcredentialbecome optional form parameters. The browser posts neither for a share, and a missing requiredForm()is FastAPI's own 422 — an error page, not the form the analyst is looking at.max_file_bytesis read as text and converted in the route, for the same reason: a mistyped ceiling re-renders the form with "must be a whole number of bytes".probe_sourcealready refused a share and explained why; the console showed a button that could only ever produce that refusal.PROBEABLE_TYPESis the API's own answer to "is there a check for this type", asked before offering one.The chip helpers do not upper-case — roots and globs are paths, where case is significant, unlike a space or project key — and a leading slash is trimmed, since a root is relative to the mount and the API answers 422 for one. Better to take the obvious meaning than to teach the rule with an error.
The details card
Describes a share rather than a site: protocol, mount, roots, include/exclude, the ceiling, and the symlink policy — with
Authreading "carried by the mount, on the engine" rather than the Cloud/Server line, which is meaningless here.Tests
Seven in
apps/api/tests/test_web_screens.py:SUPPORTED_SOURCE_TYPESrather than a list repeated in the test — a type the API supports and the select omits is exactly the state this fixes, and it should fail the suite next time;credential_ref is None, and the page explains the absence;<dt>Base URL</dt>;Two superseded tests:
test_editing_a_source_the_form_cannot_express_is_refused_not_a_500pinned the refusal this PR removes and is replaced by "editing a share saves it".test_a_hand_posted_type_without_a_form_is_refused_not_a_500is rewritten astest_a_body_that_does_not_match_the_posted_type_is_refused_not_a_500— with every supported type now on the form, the reachable failure is a hand-posted body that doesn't match its type, and that still must not 500. The unreachableelse:branch in_connection_formstays, marked# pragma: no cover, as the landing spot for the next connector the API supports before the console catches up.make checkgreen: ruff, mypy, docs check, 1961 passed / 2 skipped.test_web_shell.pyandtest_web_invariants.pypass unchanged — the new partial carries no inline script or style, and all behaviour is intags.js.Generated by Claude Code