diff --git a/frontend/src/components/custom-rules.tsx b/frontend/src/components/custom-rules.tsx index a4f2a1f..7c24397 100644 --- a/frontend/src/components/custom-rules.tsx +++ b/frontend/src/components/custom-rules.tsx @@ -15,7 +15,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { IconDeviceFloppy, IconHistory, IconFileText, IconTerminal, IconTestPipe, IconLock, IconCrown } from "@tabler/icons-react"; +import { IconDeviceFloppy, IconHistory, IconFileText, IconTerminal, IconTestPipe, IconCrown } from "@tabler/icons-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { ExecutionLogsSheet } from "@/components/execution-logs"; @@ -468,196 +468,184 @@ export function CustomRules() { }, []); return ( -
- {/* Integrated Toolbar */} -
-
-
- - rules.ts -
- {!isFreeTier && ( -
- - Plus Feature -
- )} - {hasUnsavedChanges && ( -
- - - - - Unsaved +
+ {isFreeTier && ( +
+
+
+
- )} -
- -
- - - - - - Version History - - {customRulesHistory.length === 0 ? ( - No history available - ) : ( - customRulesHistory.map((version, index) => ( - handleRestoreVersion(version.value)} - className="flex flex-col items-start gap-0.5 py-2" - > -
- Version {version.version} - - {index === 0 && version.value === customRules - ? "CURRENT" - : formatDate(version.created_at)} - -
-
- )) - )} -
-
- - - - - + Custom Rules are available on Plus or Pro plans. Upgrade to execute advanced logic. +
-
+ )} - {/* Draft restoration banner */} - {showDraftBanner && ( -
+
+ {/* Integrated Toolbar */} +
- - - Restorable draft found from a previous session. - +
+ + rules.ts +
+ {!isFreeTier && ( +
+ + Plus Feature +
+ )} + {hasUnsavedChanges && ( +
+ + + + + Unsaved +
+ )}
+
- + + + Version History + + {customRulesHistory.length === 0 ? ( + No history available + ) : ( + customRulesHistory.map((version, index) => ( + handleRestoreVersion(version.value)} + className="flex flex-col items-start gap-0.5 py-2" + > +
+ Version {version.version} + + {index === 0 && version.value === customRules + ? "CURRENT" + : formatDate(version.created_at)} + +
+
+ )) + )} +
+ + + + + Exec Logs + + + +
- )} -
- - - {/* Free Tier Overlay */} - {isFreeTier && ( -
-
-
- -
-
-

Custom Rules is a Pro feature

-

- Write your own logic to handle infinite edge cases. Allow research on youtube, - block specific subreddits, or give yourself a 15 minute break every 2 hours. -

-
-
- -
+ {/* Draft restoration banner */} + {showDraftBanner && ( +
+
+ + + Restorable draft found from a previous session. + +
+
+ +
)} -
- - +
+ +
+ + + +
); } diff --git a/frontend/src/components/settings/extensions-settings.tsx b/frontend/src/components/settings/extensions-settings.tsx index 9aa5d40..257fd55 100644 --- a/frontend/src/components/settings/extensions-settings.tsx +++ b/frontend/src/components/settings/extensions-settings.tsx @@ -1,33 +1,87 @@ import { Card, CardContent, - CardHeader, - CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { useState } from "react"; import { IconTerminal, IconCopy, IconCheck, IconRobot, - IconPlug, - IconPlus, IconBook, - IconActivity, IconLock, - IconStar + IconCode } from "@tabler/icons-react"; -import { Browser } from "@wailsio/runtime"; +import { Browser, Clipboard } from "@wailsio/runtime"; import { useAccountStore } from "@/stores/account-store"; import { useQuery } from "@tanstack/react-query"; import { DeviceHandshakeResponse_AccountTier } from "../../../bindings/github.com/focusd-so/focusd/gen/api/v1/models"; const PORT = 50533; +const API_EXAMPLES = [ + { + id: "whitelist", + title: "Whitelist a site", + method: "POST", + path: "/whitelist", + description: "Temporarily allow access to a specific domain while in focus mode. Useful for giving agents access to docs.", + code: `curl -X POST http://localhost:50533/whitelist \\ + -H "Content-Type: application/json" \\ + -d '{ + "hostname": "example.com", + "duration_seconds": 3600 + }'` + }, + { + id: "unwhitelist", + title: "Remove from whitelist", + method: "POST", + path: "/unwhitelist", + description: "Remove a previously whitelisted domain from the allowed list to re-enable blocking.", + code: `curl -X POST http://localhost:50533/unwhitelist \\ + -H "Content-Type: application/json" \\ + -d '{ + "id": 1 + }'` + }, + { + id: "pause", + title: "Pause Focus", + method: "POST", + path: "/pause", + description: "Temporarily pause your current focus session for a specific duration.", + code: `curl -X POST http://localhost:50533/pause \\ + -H "Content-Type: application/json" \\ + -d '{ + "duration_seconds": 300 + }'` + }, + { + id: "unpause", + title: "Unpause Focus", + method: "POST", + path: "/unpause", + description: "Resume your focus session immediately, canceling any active pause.", + code: `curl -X POST http://localhost:50533/unpause` + }, + { + id: "status", + title: "Get Status", + method: "GET", + path: "/status", + description: "Retrieve the current status of Focusd, including active sessions, pauses, and whitelists.", + code: `curl -X GET http://localhost:50533/status` + } +]; + export function ExtensionsSettings() { const [copied, setCopied] = useState(null); + const [selectedExampleId, setSelectedExampleId] = useState(API_EXAMPLES[0].id); + const { checkoutLink, fetchAccountTier } = useAccountStore(); const { data: accountTier } = useQuery({ @@ -40,16 +94,18 @@ export function ExtensionsSettings() { const baseUrl = `http://localhost:${PORT}`; const copyToClipboard = (text: string, label: string) => { - navigator.clipboard.writeText(text); + Clipboard.SetText(text); setCopied(label); setTimeout(() => setCopied(null), 2000); }; + const activeEx = API_EXAMPLES.find(ex => ex.id === selectedExampleId) || API_EXAMPLES[0]; + return ( -
- {/* Locked Banner Replacement */} +
+ {/* Locked Banner */} {isLocked && ( -
+
@@ -66,201 +122,124 @@ export function ExtensionsSettings() {
)} - {/* Hero Section - More Compact */} -
-
-
-
- - Plus Feature - -
-

- Extend Focusd with Local API -

-

- Automate your focus workflow and integrate Focusd with third-party tools, coding agents, and custom scripts. Our Local API provides the building blocks for a customized productivity environment. -

-
- {[ - { icon: IconRobot, label: "AI Agents" }, - { icon: IconTerminal, label: "Custom Scripts" }, - { icon: IconPlug, label: "Workflow Tools" }, - ].map((item, idx) => ( -
- - {item.label} + {/* Top Section: Endpoint & Info */} +
+ + +
+
+ +
+
+
+ Local API Endpoint + {!isLocked && ( + + Plus Feature + + )}
- ))} -
-
-
-
- +
{baseUrl}
+
-
-
+ + + - {/* Background blobs for aesthetics - smaller */} -
-
+
+
+

+ + Extensible Workflow +

+

+ Automate focus with local scripts or let coding agents manage blocking automatically while researching. +

+
-
- {/* Main Content */} -
- - -
-
- -
-
- Local API Endpoint -
-
-
- -
-
Base URL
-
-
- {baseUrl} - -
-
-
-
-
+ {/* Split View for Examples */} + +
- - -
-
- -
-
- Usage Examples -
-
-
- -
-
-
Whitelist a site
- POST /whitelist -
-
-
-                    {`curl -X POST ${baseUrl}/whitelist \\
-  -H "Content-Type: application/json" \\
-  -d '{"hostname":"x.com","duration_seconds":3600}'`}
-                  
- -
+
{ex.title}
+ + {ex.method} + + + ))}
+ +
-
-
-
Remove from whitelist
- POST /unwhitelist -
-
-
-                    {`curl -X POST ${baseUrl}/unwhitelist \\
-  -H "Content-Type: application/json" \\
-  -d '{"id":1}'`}
-                  
+ {/* Main Content Area */} +
+
+
+

{activeEx.title}

+ + {activeEx.method} + {activeEx.path} + +
+

{activeEx.description}

+
+ +
+
+
+
+ + cURL format +
-
- -
-
-
Quick Reference:
- {["/pause", "/unpause", "/whitelist", "/unwhitelist"].map((path) => ( - - {path} - - ))} +
+
+                    {activeEx.code}
+                  
- - -
+
+
- {/* Sidebar Info */} -
- - -
- - How it works -
- Extensible Workflow -
- -

- The Local API allows external tools to query and modify your focus state. This is perfect for the AGENT ERA. -

-
- {[ - { title: "Agents", desc: "can pause blocking while they research on your behalf." }, - { title: "Scripts", desc: "can automate blocking based on your specific dev environment state." }, - { title: "Dashboards", desc: "can pull your focus stats for custom displays." }, - ].map((item, idx) => ( -
-
-

{item.title} {item.desc}

-
- ))} -
-
- -
- -
-
+
); } diff --git a/internal/usage/http_handler.go b/internal/usage/http_handler.go index f730d72..45da7a1 100644 --- a/internal/usage/http_handler.go +++ b/internal/usage/http_handler.go @@ -5,6 +5,9 @@ import ( "net/http" "time" + apiv1 "github.com/focusd-so/focusd/gen/api/v1" + "github.com/focusd-so/focusd/internal/identity" + "github.com/go-chi/chi/v5" ) @@ -29,6 +32,16 @@ type UnwhitelistRequest struct { func (s *Service) RegisterHTTPHandlers(r *chi.Mux) { r.Group(func(r chi.Router) { + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if identity.GetAccountTier() == apiv1.DeviceHandshakeResponse_ACCOUNT_TIER_FREE { + http.Error(w, `{"error": "This local API feature requires a Focusd Plus or Pro plan."}`, http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) + }) + r.Post("/pause", func(w http.ResponseWriter, r *http.Request) { var pauseRequest PauseRequets