mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
c7bcfd8655
Major project-dashboard release. Five new widget types (markdown_file, log_tail, cron_status, image, status_grid), inline sparkline on stat, typed status enum shared by list + status_grid, structured WidgetErrorCard, and a project-wide .scarf/ directory watch that picks up files cron jobs write next to dashboard.json. - ProjectDashboard: extend DashboardWidget with path/lines/jobId/cells/gridColumns/sparkline; add StatusGridCell + ListItemStatus (lenient parse with synonyms) - HermesFileWatcher: watch each project's .scarf/ dir alongside dashboard.json (local FSEvents + remote SSH mtime poll); updateProjectWatches signature now takes dashboardPaths + scarfDirs - New widget views: CronStatus, Image, LogTail, MarkdownFile, StatusGrid, plus WidgetErrorCard for structured failure messaging; legacy "Unknown" placeholder replaced everywhere - WidgetPathResolver: project-root-anchored path resolution that rejects absolute paths + ".." escapes pre and post canonicalization - Stat widget gains optional inline sparkline (pure SwiftUI Path, no Charts dep); list widget rows route through typed status with semantic icons + ScarfColor tints - iOS list widget + unsupported card adopt typed status + warning-toned error card (parity with Mac error styling); new widget types remain Mac-only - Site mirror: widgets.js renders all five new types (file-reading widgets show annotated catalog placeholders), sparkline SVG, status-grid grid; styles.css adds typed-status palette + error-card + sparkline + grid styles - Catalog validator: tools/widget-schema.json is the single source of truth; build-catalog.py loads it and enforces per-type required fields. 8 new test cases in test_build_catalog.py covering schema load, v2.7 additions, and missing-required rejection - Template-author skill (SKILL.md) gains v2.7 Widget Catalog section + canonical status guidance; CONTRIBUTING.md points authors at widget-schema.json; template-author bundle rebuilt - Localizable.xcstrings picks up auto-extracted strings for the previously-shipped OAuth keepalive feature - Release notes drafted at releases/v2.7.0/RELEASE_NOTES.md Backwards compatible — existing dashboard.json renders byte-identically, status synonyms (ok/up/down/active/etc.) keep working. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
3.1 KiB
Swift
68 lines
3.1 KiB
Swift
import SwiftUI
|
|
|
|
/// Project root the dashboard widgets resolve relative `path` fields against.
|
|
/// Set by `ProjectsView` from the currently-selected project; nil when no
|
|
/// project is active. v2.7+ file-reading widgets (markdown_file, log_tail,
|
|
/// image-local) read this via the environment.
|
|
private struct SelectedProjectRootKey: EnvironmentKey {
|
|
static let defaultValue: String? = nil
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var selectedProjectRoot: String? {
|
|
get { self[SelectedProjectRootKey.self] }
|
|
set { self[SelectedProjectRootKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
/// Resolves a widget's `path` field against the project root. Rejects paths
|
|
/// that escape the project boundary via `..` segments after normalization,
|
|
/// rejects absolute paths, and rejects empty / nil inputs. The returned
|
|
/// path is suitable to hand to `transport.readFile`.
|
|
///
|
|
/// Returns nil + the reason if the path is invalid; widgets surface that
|
|
/// reason via `WidgetErrorCard`.
|
|
enum WidgetPathResolver {
|
|
enum ResolveError: Error, Equatable {
|
|
case noProject
|
|
case missingPath
|
|
case absolutePath
|
|
case escapesProject
|
|
}
|
|
|
|
static func resolve(_ relativePath: String?, projectRoot: String?) -> Result<String, ResolveError> {
|
|
guard let projectRoot, !projectRoot.isEmpty else { return .failure(.noProject) }
|
|
guard let relativePath, !relativePath.isEmpty else { return .failure(.missingPath) }
|
|
if relativePath.hasPrefix("/") { return .failure(.absolutePath) }
|
|
// Strip a single leading "./" — common in template-authored paths.
|
|
let trimmed = relativePath.hasPrefix("./") ? String(relativePath.dropFirst(2)) : relativePath
|
|
// Walk the segments and reject any "..": the project root is the
|
|
// trust boundary, anything reaching outside it is rejected. We do
|
|
// this BEFORE join+standardize so symlink games can't smuggle a
|
|
// ".." through path canonicalization.
|
|
let segments = trimmed.split(separator: "/", omittingEmptySubsequences: true)
|
|
for s in segments where s == ".." { return .failure(.escapesProject) }
|
|
let joined = (projectRoot as NSString).appendingPathComponent(trimmed)
|
|
let standardized = (joined as NSString).standardizingPath
|
|
// Belt and suspenders: ensure the standardized path is still
|
|
// beneath projectRoot. Standardizing resolves "./" and may follow
|
|
// symlinks; this catch checks the final string prefix.
|
|
let rootStd = (projectRoot as NSString).standardizingPath
|
|
if !standardized.hasPrefix(rootStd) {
|
|
return .failure(.escapesProject)
|
|
}
|
|
return .success(standardized)
|
|
}
|
|
}
|
|
|
|
extension WidgetPathResolver.ResolveError {
|
|
var userMessage: String {
|
|
switch self {
|
|
case .noProject: return "No project selected."
|
|
case .missingPath: return "Missing required `path` field."
|
|
case .absolutePath: return "Path must be relative to the project root, not absolute."
|
|
case .escapesProject: return "Path escapes the project root (`..` segments are not allowed)."
|
|
}
|
|
}
|
|
}
|