Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cf3aa45
Boiler code for the C backend rewrite
chrissuu Jan 2, 2026
3807b2f
Tag C API
chrissuu Jan 3, 2026
13ba5e8
dll, map, and vec definitions
chrissuu Jan 4, 2026
c412f49
Topological sorting
chrissuu Jan 5, 2026
960c306
Debugging
chrissuu Jan 7, 2026
7cd03ac
core scheduler header file
chrissuu Jan 15, 2026
bc6732e
chore: simplify c backend
chrissuu Jan 16, 2026
87bbf53
finalized data structures
chrissuu Jan 16, 2026
378337a
DST agnostic
chrissuu Jan 7, 2026
b7ec58f
Update weekly slot creation
chrissuu Jan 7, 2026
ee70a01
Dynamic end time editing
chrissuu Jan 7, 2026
cdd055f
Some behavior changes and bug fixes
chrissuu Jan 7, 2026
7276b65
Deletion button in hover popup
chrissuu Jan 7, 2026
463eab2
Change edit blob and add blob to be a popup window rather than appear…
chrissuu Jan 7, 2026
1982a77
Better support for annual recurrences
chrissuu Jan 8, 2026
fbf6207
Help menu and prettify settings
chrissuu Jan 8, 2026
9266472
dockerized app. moved backend into its own folder
chrissuu Jan 8, 2026
5cf3cf3
Preference learning
chrissuu Jan 10, 2026
0676a99
Revise README with enhanced project details
chrissuu Jan 12, 2026
8441679
[Fix] fixed some rendering bugs related to delta occurrences
chrissuu Jan 11, 2026
dd9ff52
[Fix] make delta recurrences DST agnostic
chrissuu Jan 12, 2026
2f75422
[Feat] cost function changes, where we implement the primitive costs
chrissuu Jan 12, 2026
cc16f4e
[Feat] add split policies and split cost primitives to the scheduling…
chrissuu Jan 12, 2026
6b004b6
[Test] add some unit tests, and debug interval error where overlap ch…
chrissuu Jan 12, 2026
5a7c07d
[Fix] fix splittable policy options that weren't correctly defaulting
chrissuu Jan 12, 2026
6c14769
[UI] some stylistic changes to how splittable policy options look
chrissuu Jan 12, 2026
5e1ac9a
[refactor] organize repository
chrissuu Jan 12, 2026
84df7bf
update dockerfile to use new core python module
chrissuu Jan 12, 2026
ce917d0
package w/ electron
chrissuu Jan 12, 2026
defdba5
[test] deprecate non-primitive cost function tests, add primitive cos…
chrissuu Jan 12, 2026
3433665
Multiple blob recurrence (#6)
chrissuu Jan 14, 2026
a30877e
Frontend/event task creation (#7)
chrissuu Jan 14, 2026
41653e4
feat: made annual date api simpler
chrissuu Jan 14, 2026
9f7db95
refactor: temporarily removed the ability to add dependencies to week…
chrissuu Jan 14, 2026
1f44923
Feature/draggable dt (#8)
chrissuu Jan 15, 2026
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
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__/
.pytest_cache/
.venv/
.git/
elastisched.db
frontend/node_modules/
offline-packages/
docs/
tests/
learning/
mcp/
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
**/.vscode
**/build
**/logs
**/node_modules
**/dist
**/.parcel-cache
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
src/elastisched/engine/bin

offline-packages

elastisched.db
elastisched.db
elastischedcopy.db
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PYTHONPATH=/app/backend

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential cmake \
&& rm -rf /var/lib/apt/lists/*

RUN mkdir -p /app/data

COPY README.md requirements.txt pyproject.toml /app/
COPY core /app/core
COPY engine /app/engine
COPY backend /app/backend
COPY frontend /app/frontend

RUN pip install --upgrade pip \
&& pip install -r requirements.txt \
&& pip install -e .

EXPOSE 8000

CMD ["uvicorn", "elastisched_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
54 changes: 48 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Elastisched
![Elastisched UI](docs/ui-preview.png)

Elastisched is a scheduling playground built around "blobs": flexible, schedulable blocks of time with constraints. The project includes a FastAPI backend and a lightweight UI for exploring day/week/month/year views.


Elastisched is a scheduling and time management software built around "blobs": flexible, schedulable blocks of time with constraints.

## Core Concepts

- Blobs represent tasks/events with a schedulable time range and optional policies.
- Blobs represent tasks/events with a schedulable time range and optional policies (which guide the scheduler).
- Policies can make blobs overlappable, invisible, or splittable to guide scheduling behavior.
- Scheduling uses simulated annealing to search for good local minima under constraints.
- Scheduling uses simulated annealing and preference learning to search for good local minima under constraints.

## Features

Expand All @@ -20,16 +18,60 @@ Elastisched is a scheduling playground built around "blobs": flexible, schedulab

## Project Layout

- `src/elastisched/api`: FastAPI backend and models
- `backend/elastisched_api`: FastAPI backend and models
- `core`: core scheduling library
- `engine`: C++ scheduler engine
- `frontend`: UI (HTML/CSS/JS)
- `learning`: preference learning and ML models
- `integrations`: integrations with other calendar apps, i.e., Google Calendar
- `mcp`: a FastMCP integration for elastisched
- `tests`: API and recurrence tests

## Quick Start

### Docker

1. `docker compose up --build`
2. Open `http://localhost:8000/ui`

### Local (no Docker)

1. Install dependencies.
2. Run the API.
3. Open the UI at `/ui` or open `frontend/index.html` directly.

## Troubleshooting

- If you see a GLIB/GCC mismatch in native builds, align your Conda GCC/G++ with the version used to compile the library.

## Miscellaneous

### Scheduler Algorithm
The scheduler algorithm uses simulated annealing, which is a form of stochastic optimization for often discrete, non-differentiable cost functions.
It will not always find the global minima, but tends to find good local minima. However, for calendars with not as many events, it will find the global
minima with high probability.

#### Lookahead
Lookahead is an important concept since it determines how many events the scheduler will have to work with at a time, affecting efficiency and solution
quality. The default is set to 2 weeks, but can be changed in settings. Whenever a blob or recurrence is added or edited, the scheduler marks the old schedule
as "dirty", prompting the user to run the scheduling algorithm.

#### Preference Learning
Apart from some the most primitive cost functions, the simulated annealer won't be able to truly capture a user's intent only through code, since
our preferences for what a good schedule might look like depends on the user and can't simply be hard coded in. Hence, elastisched also implements
preference learning. The most primitive cost functions currently implemented are: illegal schedule cost (which may be deprecated to prevent illegal
schedules entirely via simulated annealer), overlapping event cost (reduce the amount of overlap for overlappable events), and split count cost
(reduce the amount of times that we split a splittable event). Naturally, most schedules conditioned by the simulated annealer are "optimal", which
implies there are many potential global optima and requires tie-breaking.

Preference learning helps to break ties by slowly learning the user's preferences over what schedules should be chosen when multiple global optima
exists.

We can view the simulated annealer as a generative algorithm which generates optimal schedules. Preference learning then runs the learned cost function
against multiple samples generated by the annealer and tries to learn the following distribution: P(U(A) > U(B)), where A and B are schedules returned by the
annealer and U is some utility function implicitly learned by the preference learning model. We can then use this learned preference function to return
the most suitable schedule in O(S) time, where S is the number of samples discovered by the simulated annealer.

#### Semantic Preference Learning
To help the preference learning algorithm, we utilize all information about an event including the description and recurrence name.
In the backend, we use a sentence-transformer model which can be optionally GPU accelerated if the user has a GPU.
3 changes: 3 additions & 0 deletions backend/elastisched_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from elastisched_api.main import app

__all__ = ["app"]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os


DEFAULT_DATABASE_URL = "sqlite+aiosqlite:///./elastisched.db"
DEFAULT_DATABASE_URL = "sqlite+aiosqlite:///./core.db"


def get_database_url() -> str:
Expand Down
17 changes: 16 additions & 1 deletion src/elastisched/api/db.py → backend/elastisched_api/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase

from elastisched.api.config import get_database_url
from elastisched_api.config import get_database_url


DATABASE_URL = get_database_url()
Expand All @@ -23,10 +23,13 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:


async def init_db() -> None:
# Ensure model metadata is registered on the current Base.
import elastisched_api.models as models # noqa: F401
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
if conn.dialect.name == "sqlite":
await _ensure_sqlite_blob_columns(conn)
await _ensure_sqlite_scheduled_occurrence_columns(conn)


async def _ensure_sqlite_blob_columns(conn) -> None:
Expand All @@ -39,3 +42,15 @@ async def _ensure_sqlite_blob_columns(conn) -> None:
missing.append(("realized_end", "DATETIME"))
for name, col_type in missing:
await conn.execute(text(f"ALTER TABLE blobs ADD COLUMN {name} {col_type}"))


async def _ensure_sqlite_scheduled_occurrence_columns(conn) -> None:
result = await conn.execute(text("PRAGMA table_info(scheduled_occurrences)"))
columns = {row[1] for row in result.fetchall()}
missing = []
if "segment_index" not in columns:
missing.append(("segment_index", "INTEGER DEFAULT 0"))
for name, col_type in missing:
await conn.execute(
text(f"ALTER TABLE scheduled_occurrences ADD COLUMN {name} {col_type}")
)
10 changes: 5 additions & 5 deletions src/elastisched/api/main.py → backend/elastisched_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from elastisched.api.db import init_db
from elastisched.api.router import router as blob_router
from elastisched.api.recurrence_router import (
from elastisched_api.db import init_db
from elastisched_api.router import router as blob_router
from elastisched_api.recurrence_router import (
occurrence_router,
recurrence_router,
)
from elastisched.api.schedule_router import schedule_router
from elastisched_api.schedule_router import schedule_router


app = FastAPI(title="Elastisched API")
_UI_DIR = Path(__file__).resolve().parents[3] / "frontend"
_UI_DIR = Path(__file__).resolve().parents[2] / "frontend"
if _UI_DIR.exists():
app.mount("/ui", StaticFiles(directory=_UI_DIR, html=True), name="ui")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlalchemy import Boolean, DateTime, Integer, JSON, String
from sqlalchemy.orm import Mapped, mapped_column

from elastisched.api.db import Base
from elastisched_api.db import Base


class BlobModel(Base):
Expand Down Expand Up @@ -36,6 +36,7 @@ class ScheduledOccurrenceModel(Base):
__tablename__ = "scheduled_occurrences"

id: Mapped[str] = mapped_column(String(200), primary_key=True)
segment_index: Mapped[int] = mapped_column(Integer, primary_key=True, default=0)
realized_start: Mapped[datetime] = mapped_column(DateTime(timezone=True))
realized_end: Mapped[datetime] = mapped_column(DateTime(timezone=True))

Expand Down
Loading