Skip to content

Latest commit

 

History

History
238 lines (161 loc) · 17 KB

architecture.md

File metadata and controls

238 lines (161 loc) · 17 KB

Architecture

Overview

Seamless Glance is a single-binary Rust TUI application. Its control flow is straightforward:

  1. parse CLI flags
  2. initialize terminal state and AWS clients
  3. load cached cost data
  4. enter the event loop
  5. refresh overview and active-view data on demand
  6. render the current screen with ratatui

The codebase is intentionally organized around one central App state object and a set of service-specific fetch modules.

Today that architecture is better at inventory and summaries than triage. A likely next evolution is to add a dedicated findings layer that consumes the existing AWS fetchers and turns raw resource data into prioritized operator actions.

That findings layer now exists in an initial form and is intentionally lightweight: it derives a small set of triage findings from already-fetched overview and security-group data, then routes operators into the relevant service views.

Module Ownership

src/main.rs

Owns:

  • CLI entrypoint
  • help and version output
  • crossterm alternate-screen and mouse-capture lifecycle (mouse capture is released while a CLI command runs and restored after, in ui::terminal)
  • keyboard and mouse event handling. The mouse wheel routes through the same App::scroll_line_up / scroll_line_down as the arrow keys, so it scrolls whatever has focus (help, an overlay, or the active view); other mouse events are ignored

src/app/mod.rs

Owns:

  • App state
  • active view selection
  • region selection and persistence
  • profile selection and persistence
  • refresh orchestration
  • cached cost loading
  • resource describe/open/SSH actions
  • overlay state

Region and profile switching share one path: rebuild_aws_clients rebuilds the SDK client bundle for the current region and current profile, so a selected profile is preserved across region changes.

This is the highest-leverage file in the app. Many user-visible workflows converge here.

src/aws/

Owns service integration and data fetching. Common patterns:

  • build or reuse AWS SDK clients
  • fetch raw AWS data
  • map AWS responses into local model structs
  • handle access denied or unavailable states where the view expects status feedback

This layer should remain primarily factual. As the product shifts toward triage, avoid burying prioritization logic deep inside fetchers unless it is tightly coupled to the AWS response itself.

Representative files:

  • src/aws/account.rs: account overview fan-out, plus assemble_overview and the summary builders that derive the overview from already-fetched inventories
  • src/aws/clients.rs: shared SDK client bundle, build_sdk_config (the single region/profile-aware config builder), and clients_for_region, the one definition global fan-outs use to build a per-region bundle. Every SDK config in the app is built here, so it is also where the retry and timeout policy lives: adaptive retry (client-side rate limiting that backs off when AWS throttles) plus connect, per-attempt, and overall operation timeouts, which matter because the app fans out across every enabled region and issues a describe per resource
  • src/aws/profiles.rs: discovers AWS profile names from the shared config and credentials files for the in-app picker
  • src/aws/cost.rs: cost explorer queries for budget, forecast, trailing spend, and usage-aware service cost insight
  • src/aws/ec2.rs: EC2 inventory and global aggregation
  • src/aws/ecr.rs: ECR repositories, each rolled up from a full DescribeImages listing (image and untagged counts, total size, latest push and recorded pull) plus its lifecycle policy and tags. A failed image listing or policy lookup is recorded as unknown (None) rather than as zero images or a missing policy, so it cannot raise a finding

src/models/

Owns app-facing types used by both fetchers and UI views:

  • resource rows
  • summary cards
  • finding rows and finding routing metadata
  • service status types
  • traits such as resource description and console linking behavior

src/ui/

Owns presentation concerns:

  • screen layout
  • headers and footers
  • shared navigation metadata and command palette rendering
  • the key registry (src/ui/keys.rs): one table of key -> action, label, help, scope. The event loop dispatches every bound character key through it, and the footer hints and help screen render from it, so an advertised binding cannot drift from the one that runs. Adding a key is one registry entry plus one arm in the dispatcher, whose exhaustive match over KeyAction means the compiler catches an unhandled action. Keys whose meaning depends on the surface (Enter, Esc, v, arrows, the SSH picker digits) stay contextual and are listed in CONTEXTUAL_KEYS so help still describes them in one place
  • theme definitions and theme switching presentation
  • individual views
  • the shared list-view renderer (src/ui/views/list_table.rs): every service list view goes through render_list_table, which owns selection clamping, the visible-height math, keeping the selected row on screen, windowing, the empty state, and table assembly. Views supply only their columns and content styling; selection styling is applied centrally so it looks the same everywhere. Note the windowing enumerates before skipping, so the index compared against the selection is the absolute row index
  • help and overlay rendering
  • transient notifications (src/ui/notification.rs): a non-modal toast for surfacing failed actions and bad command input, since eprintln! is invisible behind the alternate screen. Call App::notify_error or App::notify_warning to raise one; it auto-dismisses after a few seconds via App::clear_expired_notification in the event loop.
  • terminal suspend/resume helpers used for shell execution

src/resources/

Owns helpers shared across domains:

  • multi-region aggregation: region_aggregate::fetch_all_regions is the single status-preserving fan-out. EC2, RDS, and Lambda all run their global fetch through it, so one region answering yields Ok with partial data, and when nothing answers a denial outranks a generic failure
  • SSH command context construction

This area is a good home for future shared action helpers such as CLI command generation or resource-specific operator pivots.

src/cache/

Currently used for cost caching. The cache now stores budget, forecast, trailing spend, and usage-aware service cost insight so cost-oriented views can open quickly without re-querying Cost Explorer every time. The cache file is keyed by profile and region (cost-<profile>-<region>.json) and the stored scope is re-checked on load, so cached spend from one profile is never shown while another profile is active.

Runtime Data Flow

Startup

At startup, main:

  • handles --help, --version, and --profile <name>
  • loads config from ~/.seamless-glance/config.json
  • loads persisted theme preference from ~/.seamless-glance/config.json
  • resolves the AWS profile from --profile or the persisted config value
  • discovers available AWS profiles for the in-app picker
  • fetches enabled AWS regions using the resolved profile; if ec2:DescribeRegions fails or returns nothing, it falls back to the documented regions::FALLBACK_REGIONS list and raises a visible warning so the operator knows global views may be incomplete
  • creates AwsClients for the selected region and profile
  • constructs App
  • loads cost data and usage-aware service cost insight from the on-disk cache only; it never calls Cost Explorer before the first frame, since the cost views force a live fetch when entered
  • triggers the first refresh

Refresh Flow

The AWS fetches run off the draw thread so the UI stays responsive and the per-service progress phases actually render. App::trigger_refresh (see src/app/refresh.rs):

  1. clears stale inventory if the account context (profile + region) changed since the held data was fetched
  2. spawns a worker task on a throwaway clone of the account context (AwsClients is cheap to clone; the SDK clients are Arc-backed)
  3. stores the receiving end of an mpsc channel on the app

The worker fetches the active view's service data, sending a RefreshUpdate per service, then Done. When the account overview is needed it runs on its own task alongside the view's fetches rather than ahead of them, and Done waits for it because findings gate on the overview. Findings is the exception: it already fetches nearly every service the overview summarizes, so it fetches only the caller identity and ECS on top and assembles the overview from its own inventories (aws::account::assemble_overview plus the per-service summary builders there), rather than fetching each service twice. Navigation (on_view_enter) reuses a fresh overview instead of refetching it, since the overview alone is about ten service fetches; a manual refresh or a profile or region switch always refetches it. A same-context refresh keeps the previous overview on screen until the new one lands; only a profile or region change clears it. For the multi-service views (Findings and Cost Savings) the independent fetches run concurrently via tokio::join!, so refresh latency is the slowest single fetch rather than the sum of ~11. Target groups and load balancers are fetched as one unit (health folding needs both) that joins the concurrent set. Each frame, App::drain_refresh_updates applies whatever has arrived (so refresh_phase advances and views populate) without ever blocking the loop. On Done it rebuilds findings and cost savings, updates last_refresh, and clears the refreshing flag.

Step 1 matters because a single-service refresh only fetches its own service but rebuild_findings reads several service inventories. Tracking the context the data was fetched under (data_context) and clearing on change ensures findings are never built from a prior region or profile's data and mislabeled with the current region.

trigger_refresh records the (profile, region, view) it is fetching for in in_flight_refresh: an identical re-trigger is deduped, while a context or view change (now possible mid-refresh, since the UI is responsive) supersedes the in-flight worker by replacing the receiver.

Cost views are a partial exception: they still rely on cached Cost Explorer data, but force a fresh reload when the active view is Cost Overview or Cost Savings.

View Entry Flow

App::on_view_enter resets selection state and schedules a refresh only when the new view's data is stale. Each applied RefreshUpdate stamps an in-memory inventory_fetched_at timestamp per service; active_view_is_fresh checks that every inventory the view needs (see required_inventories) was fetched within INVENTORY_TTL. If so, the cached data is served with no AWS call; otherwise the spawned worker refetches. The freshness map is cleared alongside the inventory on a profile or region change, so cached data is never served across contexts. A manual refresh (r) and profile/region switches call trigger_refresh directly and always refetch. The header shows the data's relative age (for example Last updated 14:32:10 UTC (2m ago)) so staleness is explicit.

Navigation Flow

Navigation is currently shared across three layers:

  1. direct key shortcuts such as f and the transitional 1 through 9
  2. a slash command palette with grouped service metadata and aliases
  3. Tab / Shift+Tab cycling through major views

The command palette, help overlay, and header/footer cues should prefer shared navigation metadata rather than hand-maintained duplicated strings.

Action Flow

User actions typically route through App helpers:

  • trigger_describe
  • trigger_cli
  • trigger_open
  • trigger_ssh
  • open_selected_finding
  • open_selected_cost_savings_opportunity

Those helpers pull the selected row, derive a description request, CLI command, or console URL, and open an overlay or external browser action as needed.

For region-aware resources, action handlers should prefer the row's own region over the currently selected UI region. This matters most in global aggregation views, where a generic fallback region can otherwise produce misleading console links or failed describe calls.

Region Model

The app maintains a list of enabled AWS regions plus a synthetic global slot.

Important nuance:

  • the global slot is a UI concept, not an AWS SDK region; it sits one index past the last real region
  • it is persisted as the global sentinel and restored on startup (the initial client bundle still binds a real region, since global fans out per region separately)
  • only some service fetchers currently aggregate across all enabled regions
  • EC2, Lambda, and RDS implement global aggregation today

When expanding global behavior, keep the user-facing label honest and update the docs.

This is especially important for a triage tool because incorrect region context breaks trust in both console and CLI pivots.

Error And Status Handling

The codebase uses a mix of strategies:

  • list fetchers return (Vec<_>, ServiceStatus); the app stores the status in a companion *_status field and the view guards on it via ui::views::status::render_unavailable before drawing the table, so an access denial or throttle reads as such rather than as an empty list
  • list fetchers page through the full result set (.into_paginator().items().send(), or a manual next-token loop where the SDK has no generated paginator, such as apigatewayv2::get_apis), so large accounts are not silently truncated to the first page
  • where a fetcher needs a follow-up describe per resource (target-group health, SQS queue attributes, ECR images and lifecycle policies), those calls run concurrently but bounded by aws::DESCRIBE_CONCURRENCY rather than one round trip at a time, so large accounts stay fast without amplifying throttling. A failed per-resource describe drops that row, as before
  • cost fetchers return (data, ServiceStatus) too; refresh_cost_data composes a single cost_status, the cost overview and cost savings views guard on it, and only a successful fetch is written to the on-disk cost cache so a denied fetch is not persisted as a misleading $0 overview
  • account overview fans out with concurrent calls and composes summary status. It needs counts, not detail, so it skips per-resource follow-ups it does not use (Lambda counts skip the per-function ListTags)
  • SDK errors are classified centrally by ServiceStatus::from_sdk_error, which inspects the error code via ProvideErrorMetadata (recognizing AccessDenied, AccessDeniedException, UnauthorizedOperation, and related authz codes) rather than substring-matching the display string

Contributors adding a new list view should follow the same shape: carry a ServiceStatus out of the fetcher, store it alongside the data, and call render_unavailable at the top of the view so AWS failures never render as misleading empty-state success UIs.

Extension Pattern

To add a new service view cleanly:

  1. create or extend the model types
  2. add a fetcher under src/aws/
  3. extend AwsClients if a new SDK client is needed
  4. add app state fields and refresh hooks in src/app/mod.rs
  5. add a src/ui/views/*.rs renderer
  6. wire navigation and command palette support
  7. update docs

To add a future triage or waste feature cleanly:

  1. keep resource collection in src/aws/
  2. derive findings from collected facts in a dedicated layer rather than directly in view code
  3. attach operator actions to each finding
  4. make severity, category, and next step obvious in the UI

The initial findings implementation follows that pattern with:

  • a Finding model in src/models/finding.rs
  • a rule registry in src/app/findings.rs: each rule is a small fn(&FindingContext) -> Vec<Finding> listed in FINDING_RULES, so rules are individually unit-testable and adding one is a function plus a registry entry rather than more inline branches. FindingContext carries borrowed inputs (the account overview and the service inventories), and sample_list is the shared "a, b, c (+N more)" helper every rule uses for its summary. Rules that read the account overview return nothing when it is absent
  • derived findings stored in App; App::rebuild_findings just builds the context and calls build_findings
  • a dedicated Findings view that routes into related service screens

The cost-savings implementation follows a similar pattern:

  • usage-aware service spend is cached through src/cache/cost.rs
  • App derives cost-savings opportunities by combining spend, usage types, and waste-oriented resource signals
  • a dedicated Cost Savings view routes operators into the underlying service screen for action

Current Gaps

  • automated tests are minimal
  • global aggregation support is incomplete across services
  • service coverage is still missing several high-value AWS domains like EBS, Elastic IPs, CloudWatch Logs, and S3
  • the findings backlog is still much smaller than the intended triage and waste catalog
  • refresh and error handling conventions are not fully uniform across all fetchers

Those are good places to tighten over time, but any changes should preserve the current operator-friendly speed and simplicity.