Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"snyk.advanced.organization": "726c6122-c974-4d1e-9c24-2d57c38c56dd",
"snyk.advanced.autoSelectOrganization": true
}
2 changes: 1 addition & 1 deletion build/darwin/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ tasks:
ref: .DEV
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS_FULL}}" -o {{.OUTPUT}}
- go build -p 1 {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS_FULL}}" -o {{.OUTPUT}}
vars:
BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false{{end}}'
LDFLAGS_FULL: '{{if eq .DEV "true"}}{{.LDFLAGS}}{{else}}-w -s {{.LDFLAGS}}{{end}}'
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/pause-confirmation-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ export function PauseConfirmationDialog({
setSelectedDuration(null);
};

const handleQuickAllow = async (executablePath: string, hostname: string, durationMinutes: number) => {
const key = hostname || executablePath;
const handleQuickAllow = async (appName: string, hostname: string, durationMinutes: number) => {
const key = hostname || appName;
setAllowingKey(key);
try {
await addToWhitelist(executablePath, hostname, durationMinutes);
await addToWhitelist(appName, hostname, durationMinutes);
onOpenChange(false);
} finally {
setAllowingKey(null);
Expand Down Expand Up @@ -173,7 +173,7 @@ export function PauseConfirmationDialog({
<button
key={durationFn}
onClick={() =>
handleQuickAllow(app?.executable_path || "", app?.hostname || "", durationFn)
handleQuickAllow(app?.name || "", app?.hostname || "", durationFn)
}
disabled={isAllowing}
className="px-2.5 py-1 text-[11px] font-medium text-muted-foreground hover:bg-green-500/10 hover:text-green-400 transition-all disabled:opacity-50"
Expand Down
14 changes: 0 additions & 14 deletions frontend/src/components/test-rules-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ export function TestRulesSheet({
onOpenChange: (open: boolean) => void;
}) {
const [appName, setAppName] = useState("");
const [executablePath, setExecutablePath] = useState("");
const [url, setUrl] = useState("");
const [datetime, setDatetime] = useState(formatDatetimeLocal(new Date()));
const [timezone, setTimezone] = useState("local");
Expand All @@ -184,7 +183,6 @@ export function TestRulesSheet({

const response = await ClassifyCustomRules(
appName.trim(),
executablePath.trim(),
urlParam,
nowTime
);
Expand Down Expand Up @@ -236,18 +234,6 @@ export function TestRulesSheet({
/>
</div>

<div className="space-y-1.5">
<label className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
Executable Path <span className="text-[10px] font-normal text-muted-foreground/40">(optional)</span>
</label>
<Input
placeholder="e.g. /Applications/Slack.app/Contents/MacOS/Slack"
value={executablePath}
onChange={(e) => setExecutablePath(e.target.value)}
className="h-9 text-sm bg-background/50 font-mono text-xs"
/>
</div>

<div className="space-y-1.5">
<label className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
URL <span className="text-[10px] font-normal text-muted-foreground/40">(optional)</span>
Expand Down
32 changes: 19 additions & 13 deletions frontend/src/components/usage-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ function formatSandboxLogs(logsStr: string | null | undefined): string {
}

export function formatDuration(seconds: number): string {
if (seconds <= 0) return "0m";
if (seconds <= 0) return "0s";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
if (m > 0) return `${m}m`;
return `${s}s`;
}

export function formatClassificationSource(
Expand Down Expand Up @@ -416,7 +418,7 @@ export function UsageItem({ usage }: { usage: ApplicationUsage }) {
usage.classification_reasoning
);

// Duration display: show when ended_at is present
// Duration display: show duration for ended items, or elapsed time for ongoing items
const durationSeconds =
usage.ended_at && usage.started_at
? usage.ended_at - usage.started_at
Expand Down Expand Up @@ -494,17 +496,16 @@ export function UsageItem({ usage }: { usage: ApplicationUsage }) {

{/* Right Side Group */}
<div className="flex items-center gap-2 shrink-0">
<div className="flex flex-col items-end gap-0.5">
<div className="flex flex-row items-center gap-1">
<span className="text-[9px] text-muted-foreground/40 font-mono">
{formatSmartDate(usage.started_at)}
{durationSeconds != null && durationSeconds > 0 && (
<>
<span className="mx-0.5">·</span>
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-1.5">
{durationSeconds != null && durationSeconds >= 0 && (
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold text-foreground/90 tabular-nums">
{formatDuration(durationSeconds)}
</>
)}
</span>
</span>
</div>
)}

<Badge
variant="outline"
className={`px-1.5 py-0 text-[9px] font-bold rounded-full ${theme.badge}`}
Expand All @@ -522,6 +523,11 @@ export function UsageItem({ usage }: { usage: ApplicationUsage }) {
</Badge>
))}
</div>

<span className="text-[10px] text-muted-foreground/50 tabular-nums leading-none">
at {formatSmartDate(usage.started_at)}
</span>

<ClassificationReasoningLabel
usage={usage}
icon={icon}
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/routes/activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ function ActivityPage() {
// For native apps (no hostname): match by bundle_id only
const whitelistEntry = allowedItems.find((w) => {
const itemHostname = item.usage.application?.hostname;
const itemExePath = item.usage.application?.executable_path;
const itemName = item.usage.application?.name;

if (w.hostname) {
// Whitelist entry is for a website - match by hostname only
return w.hostname === itemHostname;
} else if (w.executable_path) {
// Whitelist entry is for a native app - match by executable_path only
return w.executable_path === itemExePath;
} else if (w.appname) {
// Whitelist entry is for a native app - match by name only
return w.appname === itemName;
}
return false;
});
Expand All @@ -181,8 +181,8 @@ function ActivityPage() {
const alreadyInList = result.some((r) => {
if (allowed.hostname) {
return r.usage.application?.hostname === allowed.hostname;
} else if (allowed.executable_path) {
return r.usage.application?.executable_path === allowed.executable_path;
} else if (allowed.appname) {
return r.usage.application?.name === allowed.appname;
}
return false;
});
Expand All @@ -192,8 +192,8 @@ function ActivityPage() {
const recentUsage = recentUsages.find((u) => {
if (allowed.hostname) {
return u.application?.hostname === allowed.hostname;
} else if (allowed.executable_path) {
return u.application?.executable_path === allowed.executable_path;
} else if (allowed.appname) {
return u.application?.name === allowed.appname;
}
return false;
});
Expand Down Expand Up @@ -323,9 +323,9 @@ function BlockedUsageItem({ item }: { item: BlockedUsageDisplay }) {
};

const handleAllowWithDuration = async (durationMinutes: number) => {
const executablePath = usage.application?.executable_path || "";
const appname = usage.application?.name || "";
const hostname = usage.application?.hostname || "";
await addToWhitelist(executablePath, hostname, durationMinutes);
await addToWhitelist(appname, hostname, durationMinutes);
};

const handleUnallow = async () => {
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/routes/screen-time/screentime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ export interface UsageAggregation {
application: {
id: number;
name: string;
executable_path: string;
icon: string;
icon: string | null;
hostname: string | null;
domain: string | null;
bundle_id: string | null;
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/stores/usage-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,7 @@ interface UsageState {

// Whitelist actions
fetchWhitelist: () => Promise<void>;
addToWhitelist: (
executablePath: string,
hostname: string,
durationMinutes: number
) => Promise<void>;
addToWhitelist: (appname: string, hostname: string, durationMinutes: number) => Promise<void>;
removeFromWhitelist: (id: number) => Promise<void>;

// Protection actions
Expand Down Expand Up @@ -311,9 +307,9 @@ export const useUsageStore = create<UsageState>()((set, get) => ({
}
},

addToWhitelist: async (executablePath, hostname, durationMinutes) => {
addToWhitelist: async (appname: string, hostname: string, durationMinutes: number) => {
try {
await Whitelist(executablePath, hostname, durationMinutes * Duration.Minute);
await Whitelist(appname, hostname, durationMinutes * Duration.Minute);
await get().fetchWhitelist();
} catch (err) {
console.error("Failed to add to whitelist:", err);
Expand Down
4 changes: 2 additions & 2 deletions internal/usage/classifier_custom_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/focusd-so/focusd/internal/settings"
)

func (s *Service) ClassifyCustomRules(ctx context.Context, appName string, executablePath string, url *string, nowTime *time.Time) (*ClassificationResponse, error) {
func (s *Service) ClassifyCustomRules(ctx context.Context, appName string, url *string, nowTime *time.Time) (*ClassificationResponse, error) {
if s.settingsService == nil {
slog.Warn("settings service is nil, skipping custom rules classification")

Expand All @@ -20,7 +20,7 @@ func (s *Service) ClassifyCustomRules(ctx context.Context, appName string, execu

slog.Info("classifying application usage with custom rules")

sandboxCtx := createSandboxContext(appName, executablePath, url)
sandboxCtx := createSandboxContext(appName, url)

if nowTime != nil {
t := *nowTime
Expand Down
Loading