- Updated the development server documentation to reflect the new port configuration. - Refactored string handling by replacing the centralized registry with dedicated selectors for better modularity and type safety. - Enhanced logging throughout the application by integrating loguru for structured logging and improved context handling. - Removed outdated files and streamlined the codebase for better maintainability. - Added new HTML parsing utilities using BeautifulSoup to improve DOM traversal and element extraction. - Updated various components to utilize the new string selectors, ensuring consistency across the codebase.
3.9 KiB
3.9 KiB
Spec Validation (2025-12-07)
Findings below are grounded in current source with inline excerpts.
Functional Gaps
- Docling config is cosmetic:
extract_ui_elementskeeps_docling_url/_api_keyparams but the docstring says they are “Unused (maintained for signature compatibility)”, and the body only runs_extract_elements_from_html(...)on captured HTML (no HTTP call).
# src/guide/app/browser/diagnostics.py
async def extract_ui_elements(..., _docling_url: str | None = None, _api_key: str | None = None):
"""... docling_url: Unused (maintained for signature compatibility)"""
html = await capture_html(page)
ui_elements = _extract_elements_from_html(html)
- OTP callbacks are process‑local: the store is an in‑memory dict plus a module‑level singleton. Requests and callbacks on different workers won’t see the same
_requests.
# src/guide/app/auth/otp_callback.py
class OtpCallbackStore:
_requests: dict[str, OtpRequest]
...
_store: OtpCallbackStore | None = None
- GraphQL client rejects partial successes: any
errorsentry triggersGraphQLOperationError, even ifdatais present.
# src/guide/app/raindrop/graphql.py
if validated.has_errors:
raise errors.GraphQLOperationError(
validated.first_error_message or "GraphQL operation failed",
details={"errors": error_details},
)
Configuration & Portability
- Demonstration board bindings are hardcoded to a single environment.
# src/guide/app/raindrop/operations/demonstration.py
DEMONSTRATION_BOARD_ID = 596
DEMONSTRATION_INSTANCE_ID = 107
- Redis URL currently unused: even with
RAINDROP_DEMO_REDIS_URL=redis://192.168.50.210:6379/4,AppSettingshas no Redis-related fields; only the documented keys (raindrop URLs, browser hosts, docling_, session_, n8n_*) are parsed, so the value is ignored at runtime.
# src/guide/app/core/config.py
model_config = SettingsConfigDict(env_prefix="RAINDROP_DEMO_", ...)
# defined fields: raindrop_base_url, raindrop_graphql_url, browser_hosts, docling_*, session_*, n8n_* (no redis)
Security Notes
- JWT expiry is parsed without signature verification and feeds offline session validation; a forged token with a far‑future
expcould keep a cached session “valid” until TTL expires.
# src/guide/app/auth/session_manager.py
token_expires_at = self.parse_jwt_expiry(token.value)
...
if session.token_expires_at:
token_remaining = session.token_expires_at - now
Performance Considerations
- Accordion collapsing issues one Playwright hop per button (
buttons.nth(index)inside a loop), which scales poorly on pages with many accordions.
# src/guide/app/browser/elements/layout.py
for index in range(count):
button: PageLocator = buttons.nth(index)
if await icon.count() > 0:
await button.click(timeout=max_wait)
- Form discovery walks every
[data-cy^="board-item-"]node and marshals its metadata in one evaluate call; large boards will produce heavy payloads.
// src/guide/app/browser/elements/form_discovery.py
const fields = container.querySelectorAll('[data-cy^=\"board-item-\"]');
return Array.from(fields)
.filter(field => field.offsetParent !== null)
DX / DI Footnote
- Action DI requires constructor params to exist in the DI context unless they’re varargs or have defaults; decorated
__init__withoutfunctools.wrapscan break injection.
# src/guide/app/actions/base.py
for param_name, param in sig.parameters.items():
if param_name in self._di_context:
kwargs[param_name] = self._di_context[param_name]
else:
is_var_param = param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD)
has_default = cast(object, param.default) != Parameter.empty
if not is_var_param and not has_default:
raise errors.ActionExecutionError(...)