Initial commit: KeepItGoing task management server

Features:
- Django-based REST API with web interface
- Task management with tags, priorities, and due dates
- Time tracking with start/stop timers
- Subtasks support
- Task filtering (all, today, upcoming, overdue, completed)
- Tag-based organization with color coding
- Sorting by due date and priority
- Auto-assign tags when filtering
- Responsive 3-pane layout (sidebar, task list, detail panel)
- Task sharing between users
- Mobile-responsive design with dark mode support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2025-12-18 17:41:29 -07:00
co-authored by Claude Sonnet 4.5
commit 6a0b35c39c
84 changed files with 7763 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
# Generated by Django 5.2.9 on 2025-12-10 00:05
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('color', models.CharField(default='#6B7280', max_length=7)),
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'tags',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Task',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=500)),
('description', models.TextField(blank=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent')], default='medium', max_length=20)),
('due_date', models.DateField(blank=True, null=True)),
('due_time', models.TimeField(blank=True, null=True)),
('reminder_at', models.DateTimeField(blank=True, null=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('recurrence', models.CharField(choices=[('none', 'None'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('biweekly', 'Bi-weekly'), ('monthly', 'Monthly'), ('yearly', 'Yearly'), ('custom', 'Custom')], default='none', max_length=20)),
('recurrence_rule', models.CharField(blank=True, max_length=500)),
('recurrence_end_date', models.DateField(blank=True, null=True)),
('sort_order', models.IntegerField(default=0)),
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'tasks',
'ordering': ['sort_order', '-priority', 'due_date', 'created_at'],
},
),
migrations.CreateModel(
name='TaskGroup',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('color', models.CharField(default='#3B82F6', max_length=7)),
('icon', models.CharField(blank=True, max_length=50)),
('sort_order', models.IntegerField(default=0)),
('is_archived', models.BooleanField(default=False)),
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'task_groups',
'ordering': ['sort_order', 'name'],
},
),
migrations.CreateModel(
name='TaskShare',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('permission', models.CharField(choices=[('viewer', 'Viewer'), ('editor', 'Editor')], default='viewer', max_length=20)),
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'task_shares',
},
),
migrations.CreateModel(
name='TimeEntry',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('started_at', models.DateTimeField()),
('ended_at', models.DateTimeField(blank=True, null=True)),
('duration_seconds', models.IntegerField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'time_entries',
'ordering': ['-started_at'],
},
),
]
+86
View File
@@ -0,0 +1,86 @@
# Generated by Django 5.2.9 on 2025-12-10 00:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('tasks', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='tag',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='task',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subtasks', to='tasks.task'),
),
migrations.AddField(
model_name='task',
name='tags',
field=models.ManyToManyField(blank=True, related_name='tasks', to='tasks.tag'),
),
migrations.AddField(
model_name='task',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='tag',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='task',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='tasks.tag'),
),
migrations.AddField(
model_name='taskshare',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='tasks.tag'),
),
migrations.AddField(
model_name='taskshare',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shares_given', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='taskshare',
name='shared_with',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shares_received', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='taskshare',
name='task',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='tasks.task'),
),
migrations.AddField(
model_name='timeentry',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='time_entries', to='tasks.task'),
),
migrations.AddField(
model_name='timeentry',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='time_entries', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='tag',
unique_together={('user', 'name')},
),
migrations.AddConstraint(
model_name='taskshare',
constraint=models.CheckConstraint(condition=models.Q(models.Q(('group__isnull', True), ('task__isnull', False)), models.Q(('group__isnull', False), ('task__isnull', True)), _connector='OR'), name='share_task_or_group_not_both'),
),
]
@@ -0,0 +1,135 @@
# Migration to merge TaskGroup into Tag and support multiple tags per task
from django.db import migrations, models
import django.db.models.deletion
def migrate_groups_to_tags(apps, schema_editor):
"""Migrate TaskGroup data to Tag and convert task.group_id to task_tags."""
TaskGroup = apps.get_model('tasks', 'TaskGroup')
Tag = apps.get_model('tasks', 'Tag')
Task = apps.get_model('tasks', 'Task')
# First, migrate all TaskGroups to Tags
group_to_tag_map = {}
for group in TaskGroup.objects.all():
# Check if a tag with the same sync_id already exists
existing_tag = Tag.objects.filter(sync_id=group.sync_id).first()
if existing_tag:
# Update existing tag with group data
existing_tag.description = group.description
existing_tag.icon = group.icon
existing_tag.sort_order = group.sort_order
existing_tag.is_archived = group.is_archived
existing_tag.save()
group_to_tag_map[group.id] = existing_tag
else:
# Create new tag from group
tag = Tag.objects.create(
user=group.user,
name=group.name,
description=group.description,
color=group.color,
icon=group.icon,
sort_order=group.sort_order,
is_archived=group.is_archived,
sync_id=group.sync_id,
)
group_to_tag_map[group.id] = tag
# Convert task.group_id to task_tags entries
for task in Task.objects.exclude(group_id__isnull=True):
if task.group_id in group_to_tag_map:
tag = group_to_tag_map[task.group_id]
task.tags.add(tag)
def reverse_migration(apps, schema_editor):
"""Reverse migration - not fully reversible, just a placeholder."""
pass
class Migration(migrations.Migration):
dependencies = [
('tasks', '0002_initial'),
]
operations = [
# Step 1: Add new fields to Tag model
migrations.AddField(
model_name='tag',
name='description',
field=models.TextField(blank=True, default=''),
preserve_default=False,
),
migrations.AddField(
model_name='tag',
name='icon',
field=models.CharField(blank=True, default='', max_length=50),
preserve_default=False,
),
migrations.AddField(
model_name='tag',
name='sort_order',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='tag',
name='is_archived',
field=models.BooleanField(default=False),
),
# Step 2: Remove unique_together constraint on Tag (user, name)
# to allow migration of groups with same names
migrations.AlterUniqueTogether(
name='tag',
unique_together=set(),
),
# Step 3: Change Tag ordering
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['sort_order', 'name']},
),
# Step 4: Run data migration
migrations.RunPython(migrate_groups_to_tags, reverse_migration),
# Step 5: Remove group field from Task
migrations.RemoveField(
model_name='task',
name='group',
),
# Step 6: Update TaskShare - remove old constraint first
migrations.RemoveConstraint(
model_name='taskshare',
name='share_task_or_group_not_both',
),
# Step 7: Rename group field to tag in TaskShare
migrations.RenameField(
model_name='taskshare',
old_name='group',
new_name='tag',
),
# Step 8: Add new constraint for TaskShare
migrations.AddConstraint(
model_name='taskshare',
constraint=models.CheckConstraint(
check=models.Q(
models.Q(('tag__isnull', True), ('task__isnull', False)),
models.Q(('tag__isnull', False), ('task__isnull', True)),
_connector='OR'
),
name='share_task_or_tag_not_both'
),
),
# Step 9: Delete TaskGroup model
migrations.DeleteModel(
name='TaskGroup',
),
]
@@ -0,0 +1,29 @@
# Generated by Django 5.2.9 on 2025-12-13 00:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0003_merge_groups_to_tags'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='color',
field=models.CharField(default='#3B82F6', max_length=7),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='taskshare',
name='tag',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='tasks.tag'),
),
]
@@ -0,0 +1,46 @@
# Generated by Django 5.2.9 on 2025-12-13 06:45
from django.db import migrations
def fix_tasks_tags_table(apps, schema_editor):
"""
Fix the tasks_tags junction table to reference the correct 'tags' table
instead of the old 'task_groups' table.
"""
db_alias = schema_editor.connection.alias
# Drop and recreate the tasks_tags table with correct foreign keys
schema_editor.execute(
"DROP TABLE IF EXISTS tasks_tags;"
)
schema_editor.execute(
"""
CREATE TABLE tasks_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id CHAR(32) NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
tag_id CHAR(32) NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
UNIQUE (task_id, tag_id)
);
"""
)
# Create indexes for performance
schema_editor.execute(
"CREATE INDEX tasks_tags_task_id_idx ON tasks_tags(task_id);"
)
schema_editor.execute(
"CREATE INDEX tasks_tags_tag_id_idx ON tasks_tags(tag_id);"
)
class Migration(migrations.Migration):
dependencies = [
('tasks', '0004_alter_tag_color_alter_tag_name_alter_taskshare_tag'),
]
operations = [
migrations.RunPython(fix_tasks_tags_table, migrations.RunPython.noop),
]
@@ -0,0 +1,61 @@
# Generated by Django 5.2.9 on 2025-12-13 07:00
from django.db import migrations
def fix_task_shares_table(apps, schema_editor):
"""
Fix the task_shares table to reference the correct 'tags' table
instead of the old 'task_groups' table.
"""
db_alias = schema_editor.connection.alias
# Drop and recreate the task_shares table with correct foreign keys
schema_editor.execute(
"DROP TABLE IF EXISTS task_shares;"
)
schema_editor.execute(
"""
CREATE TABLE task_shares (
id CHAR(32) NOT NULL PRIMARY KEY,
permission VARCHAR(20) NOT NULL,
sync_id CHAR(32) NOT NULL UNIQUE,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
owner_id CHAR(32) NOT NULL REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED,
shared_with_id CHAR(32) NOT NULL REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED,
task_id CHAR(32) NULL REFERENCES tasks(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
tag_id CHAR(32) NULL REFERENCES tags(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT share_task_or_tag_not_both CHECK (
(tag_id IS NULL AND task_id IS NOT NULL) OR
(tag_id IS NOT NULL AND task_id IS NULL)
)
);
"""
)
# Create indexes for foreign keys
schema_editor.execute(
"CREATE INDEX task_shares_owner_id_idx ON task_shares(owner_id);"
)
schema_editor.execute(
"CREATE INDEX task_shares_shared_with_id_idx ON task_shares(shared_with_id);"
)
schema_editor.execute(
"CREATE INDEX task_shares_task_id_idx ON task_shares(task_id);"
)
schema_editor.execute(
"CREATE INDEX task_shares_tag_id_idx ON task_shares(tag_id);"
)
class Migration(migrations.Migration):
dependencies = [
('tasks', '0005_fix_tasks_tags_table'),
]
operations = [
migrations.RunPython(fix_task_shares_table, migrations.RunPython.noop),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.2.9 on 2025-12-14 03:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0006_fix_task_shares_table'),
]
operations = [
migrations.AddField(
model_name='tag',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='task',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='timeentry',
name='is_deleted',
field=models.BooleanField(default=False),
),
]
View File