# Project export: Locking In

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: The first app that let's you actually LOCK IN!
- Devpost: https://devpost.com/software/locking-in
- GitHub: https://github.com/scidroid/lockingin
- Video: https://www.youtube.com/embed/XsCfHLLh_XU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Juan Almanza (3 commits), Muifrend (1 commits)

## Devpost submission (written by the team)

### Inspiration

We all know that feeling: you open your laptop to study... and suddenly it’s three hours later, you’ve watched five cooking videos, and somehow you’re reading about penguin migration patterns. So we made Locking In, an app that keeps you from wandering into the depths of distraction — by literally watching you (nicely).

### What it does

You tell the app what you’re supposed to be doing — “study math,” “code,” “pretend to study math while coding,” whatever. Then the app starts taking screenshots every few seconds to see if you’re actually working or, uh, "researching" memes again. Stay focused? You earn points. Get distracted? Lose points. Your friends can see your score, so there’s extra motivation not to go down a YouTube rabbit hole.

### How we built it

We built both the client and server sides, glued them together with an API, and sprinkled in some AI magic to figure out what you’re doing on your screen.

### Challenges we ran into

Gemini API had an update that broke everything right when we thought we were done. So yeah, we got some surprise learning opportunities.

### Accomplishments we're proud of

The app gives feedback in around 20–30 seconds — fast enough to catch your distraction before you’ve fully spiraled into Wikipedia.

### What we learned

The MacOS accessibility API is... an adventure. Let’s just say it taught us patience, resilience, and a deep appreciation for working documentation.

### What's next

The core app works, but we’re dreaming big — think “social media meets productivity.” Imagine liking your friend’s focus streaks, sending accountability memes, or competing to see who can stay locked in the longest (without crying).

## README (from the GitHub repository)

# Locking In

Imagine github graph but it actually tells you something.

You have to run both server and client.

This project uses Bun and Cloudflare.

Check the README.md files in the server and client folders for more information.

## Detected evidence (automated analysis)

Indexed codebase: 40 recognized source files, 109 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- Rust (language) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (60 of 60)

```
client/.gitignore
client/.vscode/extensions.json
client/bun.lock
client/index.html
client/package.json
client/README.md
client/src-tauri/.gitignore
client/src-tauri/build.rs
client/src-tauri/capabilities/default.json
client/src-tauri/Cargo.lock
client/src-tauri/Cargo.toml
client/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
client/src-tauri/icons/android/values/ic_launcher_background.xml
client/src-tauri/icons/icon.icns
client/src-tauri/Info.plist
client/src-tauri/src/lib.rs
client/src-tauri/src/main.rs
client/src-tauri/tauri.conf.json
client/src/App.css
client/src/App.tsx
client/src/components/dash/about.tsx
client/src/components/dash/layout.tsx
client/src/components/dash/sessions/core.tsx
client/src/components/dash/sessions/history.tsx
client/src/components/dash/sessions/index.tsx
client/src/components/dash/social.tsx
client/src/components/login.tsx
client/src/components/not-locked-in-modal.tsx
client/src/components/permissions.tsx
client/src/components/rules.tsx
client/src/components/spinner.tsx
client/src/globals.ts
client/src/hooks/auth.tsx
client/src/hooks/local-storage.ts
client/src/hooks/permissions.ts
client/src/main.tsx
client/src/vite-env.d.ts
client/tsconfig.json
client/tsconfig.node.json
client/vite.config.ts
README.md
server/.gitignore
server/bun.lock
server/drizzle.config.ts
server/drizzle/migrations/0000_oval_masked_marvel.sql
server/drizzle/migrations/meta/_journal.json
server/drizzle/migrations/meta/0000_snapshot.json
server/package.json
server/README.md
server/src/db/schema.ts
server/src/index.ts
server/src/lib/ai-analysis.ts
server/src/lib/auth.ts
server/src/routes/auth.ts
server/src/routes/blob.ts
server/src/routes/sessions.ts
server/src/types.ts
server/src/utils.ts
server/tsconfig.json
server/wrangler.jsonc
```

### Dependencies

- client/package.json: @tailwindcss/vite@^4.1.16, @tauri-apps/api@^2, @tauri-apps/cli@^2, @tauri-apps/plugin-opener@^2, @types/react@^19.1.8, @types/react-dom@^19.1.6, @vitejs/plugin-react@^4.6.0, lucide-react@^0.548.0, react@^19.1.0, react-dom@^19.1.0, swr@^2.3.6, tailwindcss@^4.1.16, typescript@~5.8.3, vite@^7.0.4
- client/src-tauri/Cargo.toml: base64@0.21, serde@1, serde_json@1, tauri@2, tauri-build@2, tauri-plugin-fs@2, tauri-plugin-opener@2
- server/package.json: @cloudflare/workers-types@^4.20251014.0, @google/genai@^1.27.0, @hono/zod-validator@^0.7.4, @libsql/client@^0.15.15, drizzle-kit@^0.31.5, drizzle-orm@^0.44.7, hono@^4.10.3, wrangler@^4.4.0, zod@^4.1.12

### Recent commits (newest first)

- Final demo
- initial server commit
- New name btw
- Starting to lock in...

## Key source files (fetched from GitHub, selected and truncated for size)

### server/package.json

```
{
  "name": "cfw-bun-hono-drizzle",
  "type": "module",
  "scripts": {
    "dev": "wrangler dev",
    "db:generate": "drizzle-kit generate:sqlite",
    "db:up": "drizzle-kit up:sqlite",
    "deploy": "wrangler deploy --minify",
    "cf-typegen": "wrangler types --env-interface CloudflareBindings"
  },
  "dependencies": {
    "@cloudflare/workers-types": "^4.20251014.0",
    "@google/genai": "^1.27.0",
    "@hono/zod-validator": "^0.7.4",
    "@libsql/client": "^0.15.15",
    "drizzle-orm": "^0.44.7",
    "hono": "^4.10.3",
    "zod": "^4.1.12"
  },
  "devDependencies": {
    "drizzle-kit": "^0.31.5",
    "wrangler": "^4.4.0"
  }
}
```

### client/package.json

```
{
  "name": "lockingin",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "tauri": "tauri dev"
  },
  "dependencies": {
    "@tailwindcss/vite": "^4.1.16",
    "@tauri-apps/api": "^2",
    "@tauri-apps/plugin-opener": "^2",
    "lucide-react": "^0.548.0",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "swr": "^2.3.6",
    "tailwindcss": "^4.1.16"
  },
  "devDependencies": {
    "@types/react": "^19.1.8",
    "@types/react-dom": "^19.1.6",
    "@vitejs/plugin-react": "^4.6.0",
    "typescript": "~5.8.3",
    "vite": "^7.0.4",
    "@tauri-apps/cli": "^2"
  }
}

```

### client/src-tauri/Cargo.toml

```
[package]
name = "lockingin"
version = "0.1.0"
description = "A Tauri App"
authors = ["Juan Almanza<juan@almanza.cc>"]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "client_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = ["protocol-asset", "macos-private-api"] }
tauri-plugin-opener = "2"
tauri-plugin-fs = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.21"


```

### client/src/main.tsx

```typescript
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "@/app";
import { AuthProvider } from "@/hooks/auth";
import { globals } from "@/globals";

import "@/app.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <AuthProvider apiUrl={globals.API_URL}>
      <App />
    </AuthProvider>
  </React.StrictMode>
);

```

### server/src/index.ts

```typescript
import { Hono } from "hono";
import { cors } from "hono/cors";

import { Env } from "@/types";

import { authRoutes } from "@/routes/auth";
import { sessionRoutes } from "@/routes/sessions";
import { blobRoutes } from "@/routes/blob";

const app = new Hono<{ Bindings: Env }>();

app.onError((err, c) => {
  console.error(`${c.req.path}: `, err);

  return c.json(
    {
      success: false,
      error: "Internal server error",
    },
    500
  );
});

app.use(
  "*",
  cors({
    origin: "*",
    allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allowHeaders: ["Content-Type", "Authorization"],
  })
);

// Mount route modules
app.route("/", authRoutes);
app.route("/sessions", sessionRoutes);
app.route("/blob", blobRoutes);

export default app;

```

### client/src/App.tsx

```typescript
import { useState } from "react";
import { Login } from "@/components/login";
import { Permissions } from "@/components/permissions";
import { Rules } from "@/components/rules";
import { Layout } from "@/components/dash/layout";
import { Spinner } from "@/components/spinner";
import { usePermissions } from "@/hooks/permissions";
import { useAuth } from "@/hooks/auth";
import { useLocalStorage } from "@/hooks/local-storage";

import "@/app.css";

export function App() {
  const { permissionsGranted, isChecking } = usePermissions();
  const { isAuthenticated, logout } = useAuth();
  const [hasSeenRules, setHasSeenRules] = useLocalStorage("seen_rules", false);

  const [demoMode, setDemoMode] = useState(false);

  if (!isAuthenticated) {
    return <Login />;
  }

  if (!hasSeenRules) {
    return <Rules onAccept={() => setHasSeenRules(true)} />;
  }

  if (isChecking) {
    return (
      <main className="flex flex-col items-center justify-center min-h-screen py-10 px-5 text-center">
        <Spinner />
      </main>
    );
  }

  if (!permissionsGranted) {
    return (
      <Permissions onPermissionsGranted={() => window.location.reload()} />
    );
  }

  return (
    <Layout
      onLogout={() => {
        logout();
        setHasSeenRules(false);
      }}
      demoMode={demoMode}
      setDemoMode={setDemoMode}
    />
  );
}

```

### client/src-tauri/src/main.rs

```rust
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    client_lib::run()
}

```

### client/src/components/dash/sessions/index.tsx

```typescript
import { useState } from "react";
import useSWR from "swr";
import { useAuth } from "@/hooks/auth";
import { SessionCore } from "./core";
import { SessionHistory } from "./history";

interface SessionsSectionProps {}

interface Session {
  id: number;
  prompt: string;
  starttime: string;
  endtime: string | null;
  userId: number;
}

interface MockSession {
  id: number;
  task: string;
  time: number;
  points: number;
  date: string;
}

export function SessionsSection({}: SessionsSectionProps) {
  const [selectedSession, setSelectedSession] = useState<"new" | number>("new");
  const { email, authenticatedFetcher } = useAuth();

  // Fetch real sessions data
  const { data: realSessions, mutate } = useSWR<Session[]>(
    email ? '/sessions' : null,
    authenticatedFetcher,
    {
      refreshInterval: 15 * 1000, // Refresh every 15 seconds
      revalidateOnFocus: true,
    }
  );


  // Convert real sessions to display format
  const convertedSessions: MockSession[] =
    realSessions?.map((session) => {
      const startTime = new Date(session.starttime);
      const isRunning = !session.endtime || session.endtime === "";

      let duration: number;
      let task: string;

      if (isRunning) {
        // Session is still running - calculate current duration
        duration = Math.floor(
          (new Date().getTime() - startTime.getTime()) / 1000
        );
        task = `🔴 ${session.prompt} (Running)`;
      } else {
        // Session has ended
        const endTime = new Date(session.endtime || "");
        duration = Math.floor((endTime.getTime() - startTime.getTime()) / 1000);
        task = session.prompt;
      }

      return {
        id: session.id,
        task: task,
        time: duration,
        points: Math.floor(duration / 60), // 1 point per minute
        date: startTime.toISOString().split("T")[0],
      };
    }) || [];

  // Use converted sessions
  const displaySessions = convertedSessions;

  const formatTime = (seconds: number) => {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = seconds % 60;
    if (hours > 0) {
      return `${hours}:${minutes.toString().padStart(2, "0")}:${secs
        .toString()
        .padStart(2, "0")}`;
    }
    return `${minutes}:${secs.toString().padStart(2, "0")}`;
  };

  return (
    <>
      {/* Sessions Sidebar Extension */}
      <div className="w-64 bg-black/20 backdrop-blur-sm border-r border-white/10 fixed left-20 top-0 h-full z-5 overflow-y-auto">
        <div className="p-4">
          <h2 className="text-lg font-bold mb-4">Sessions</h2>

          {/* Start New Session Button */}
          <button
            onClick={() => setSelectedSession("new")}
            className={`w-full p-3 rounded-lg mb-4 text-left transition-all ${
              selectedSession === "new"
                ? "bg-white text-[#ff3a3a]"
                : "bg-white/10 text-white hover:bg-white/20"
            }`}
          >
            <div className="flex items-center space-x-3">
              <div className="w-6 h-6 flex items-center justify-center text-sm font-bold">
                +
              </div>
              <p className="font-medium">New Session</p>
            </div>
          </button>

          {/* Sessions */}
          {displaySessions.length > 0 && (
            <div className="space-y-2 mt-4">
              {displaySessions.map((session) => {
                // Check if this session is currently running (no endtime in real data)
                const isRunning = realSessions?.find(s => s.id === session.id && !s.endtime);

                return (
                  <button
                    key={session.id}
                    onClick={() => setSelectedSession(session.id)}
                    className={`w-full p-3 rounded-lg text-left transition-all ${
                      selectedSession === session.id
                        ? "bg-white text-[#ff3a3a]"
                        : isRunning
                        ? "bg-white/20 text-white hover:bg-white/30 border border-green-400/50"
                        : "bg-white/5 text-white hover:bg-white/10"
                    }`}
                  >
                    <div className="space-y-1">
                      <p className="font-medium text-sm truncate">
                        {session.task}
                      </p>
                      <div className="flex justify-between text-xs opacity-60">
                        <span>{formatTime(session.time)}</span>
                        <span>{session.points} pts</span>
                      </div>
                      <p className="text-xs opacity-50">{session.date}</p>
                      {isRunning && (
                        <p className="text-xs text-green-400 font-medium">● Active</p>
                      )}
                    </div>
                  </button>
                );
              })}
            </div>
          )}
        </div>
      </div>

      {/* Main Content */}
      <div className="ml-64 flex-1 flex items-center justify-center min-h-screen">
        {selectedSession === "new" ? (
          <SessionCore
            formatTime={formatTime}
            mutate={mutate}
          />
        ) : (
          <SessionHistory
            selectedSession={selectedSession}
            displaySessions={displaySessions.map(session => ({
              id: session.id,
              prompt: session.task,
              starttime: new Date(session.date).toISOString(),
              endtime: realSessions?.find(s => s.id === session.id)?.endtime || undefined,
              userId: 1
            }))}
          />
        )}
      </div>
    </>
  );
}

```

### server/drizzle.config.ts

```typescript
import { defineConfig } from "drizzle-kit";
export default defineConfig({
  dialect: "sqlite",
  schema: "./src/db/schema.ts",
  out: "./drizzle/migrations",
  dbCredentials: {
    url: ".wrangler/state/v3/d1/miniflare-D1DatabaseObject/266203f5-1064-4163-b041-bfe6ae16eb4a.sqlite"
  }
});

```

### client/index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap"
      rel="stylesheet"
    />
    <title>Locking In</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

[30 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]