Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

227 lines
8.9 KiB
Python

from django.contrib import admin, messages
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import path, reverse
from django.utils.html import format_html
from insights.storage import object_storage
from . import loop_service
from .models import CodeInvite, CodeInviteRedemption, Loop, LoopTrigger, SandboxSnapshot, Task, TaskRun
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ("slug", "title", "origin_product", "internal", "team", "created_by", "created_at", "deleted")
list_filter = ("origin_product", "internal", "deleted", "created_at")
search_fields = ("title", "description", "repository")
readonly_fields = ("id", "slug", "task_number", "created_at", "updated_at", "deleted_at")
autocomplete_fields = ("team", "created_by", "github_integration", "github_user_integration")
fieldsets = (
(None, {"fields": ("id", "slug", "task_number", "title", "description", "origin_product", "internal")}),
("Team & User", {"fields": ("team", "created_by")}),
("Repository", {"fields": ("github_integration", "github_user_integration", "repository")}),
("Schema", {"fields": ("json_schema",)}),
("Status", {"fields": ("deleted", "deleted_at")}),
("Dates", {"fields": ("created_at", "updated_at")}),
)
@admin.register(TaskRun)
class TaskRunAdmin(admin.ModelAdmin):
list_display = ("id", "task", "status", "environment", "stage", "created_at")
list_filter = ("status", "environment", "created_at")
search_fields = ("task__title", "branch", "stage")
readonly_fields = ("id", "created_at", "updated_at", "completed_at", "download_logs_link")
autocomplete_fields = ("task",)
fieldsets = (
(None, {"fields": ("id", "task", "status", "environment", "stage", "branch")}),
("Storage", {"fields": ("error_message", "download_logs_link")}),
("Data", {"fields": ("output", "state")}),
("Dates", {"fields": ("created_at", "updated_at", "completed_at")}),
)
def get_urls(self) -> list:
# Prepended so it isn't shadowed by the default `<path:object_id>/` route.
return [
path(
"<uuid:run_id>/download-logs/",
self.admin_site.admin_view(self.download_logs),
name="tasks_taskrun_download_logs",
),
*super().get_urls(),
]
def download_logs(self, request: HttpRequest, run_id) -> HttpResponse:
run = self.get_object(request, run_id)
if run is None:
raise Http404("Task run not found")
if object_storage.head_object(run.log_url) is None:
self.message_user(
request,
"No logs available for this run — they may not have been written yet, or object storage is unreachable.",
level=messages.WARNING,
)
return redirect(reverse("admin:tasks_taskrun_change", args=[run_id]))
filename = f"run_{run.id}.jsonl"
url = object_storage.get_presigned_url(
run.log_url,
expiration=300,
content_disposition=f'attachment; filename="{filename}"',
)
if not url:
self.message_user(
request,
"Could not generate a download link for this run's logs (object storage unavailable).",
level=messages.WARNING,
)
return redirect(reverse("admin:tasks_taskrun_change", args=[run_id]))
return HttpResponseRedirect(url)
@admin.display(description="Logs")
def download_logs_link(self, obj: TaskRun) -> str:
if not obj or not obj.pk:
return "—"
url = reverse("admin:tasks_taskrun_download_logs", args=[obj.pk])
return format_html('<a class="button" href="{}">Download logs</a>', url)
@admin.register(SandboxSnapshot)
class SandboxSnapshotAdmin(admin.ModelAdmin):
list_display = ("external_id", "status", "created_at", "updated_at")
list_filter = ("status", "created_at")
search_fields = ("external_id", "repos")
readonly_fields = ("id", "external_id", "created_at", "updated_at")
fieldsets = (
(None, {"fields": ("id", "external_id", "status")}),
("Repository Info", {"fields": ("repos",)}),
("Metadata", {"fields": ("metadata",)}),
("Dates", {"fields": ("created_at", "updated_at")}),
)
class CodeInviteRedemptionInline(admin.TabularInline):
model = CodeInviteRedemption
extra = 0
can_delete = False
readonly_fields = ("id", "user", "organization", "redeemed_at")
@admin.register(CodeInvite)
class CodeInviteAdmin(admin.ModelAdmin):
list_display = (
"code",
"description",
"is_active",
"redemption_count",
"max_redemptions",
"expires_at",
"created_at",
)
list_filter = ("is_active", "created_at")
search_fields = ("code", "description")
readonly_fields = ("id", "redemption_count", "created_at")
autocomplete_fields = ("created_by",)
inlines = []
def get_fieldsets(self, request, obj=None):
if obj:
return (
(None, {"fields": ("id", "code", "description")}),
("Limits", {"fields": ("is_active", "max_redemptions", "redemption_count", "expires_at")}),
("Metadata", {"fields": ("created_by", "created_at")}),
)
# On add, code may be set manually or left blank to auto-generate on save
return (
(None, {"fields": ("id", "code", "description")}),
("Limits", {"fields": ("is_active", "max_redemptions", "expires_at")}),
("Metadata", {"fields": ("created_by",)}),
)
def get_inlines(self, request, obj=None):
return [CodeInviteRedemptionInline]
@admin.register(CodeInviteRedemption)
class CodeInviteRedemptionAdmin(admin.ModelAdmin):
list_display = ("invite_code", "user", "organization", "redeemed_at")
list_filter = ("redeemed_at",)
search_fields = ("user__email", "invite_code__code")
readonly_fields = ("id", "invite_code", "user", "organization", "redeemed_at")
@admin.register(Loop)
class LoopAdmin(admin.ModelAdmin):
list_display = (
"name",
"visibility",
"enabled",
"team",
"created_by",
"last_run_at",
"last_run_status",
"consecutive_failures",
"deleted",
)
list_filter = ("visibility", "enabled", "deleted", "overlap_policy", "runtime_adapter")
search_fields = ("name", "description")
readonly_fields = (
"id",
"enabled",
"last_run_at",
"last_run_status",
"last_error",
"consecutive_failures",
"created_at",
"updated_at",
)
autocomplete_fields = ("team", "created_by", "creator")
raw_id_fields = ("sandbox_environment",)
def get_queryset(self, request: HttpRequest):
# Admin has no team context; Loop's default manager is fail-closed.
return Loop.objects.unscoped().select_related("team", "created_by")
def delete_model(self, request: HttpRequest, obj: Loop) -> None:
# Tear down Temporal Schedules before the row is gone; CASCADE never talks to Temporal, so
# a raw admin delete would otherwise leave the schedules firing forever.
loop_service.delete_loop_schedules(obj)
super().delete_model(request, obj)
def delete_queryset(self, request: HttpRequest, queryset) -> None:
for loop in queryset:
loop_service.delete_loop_schedules(loop)
super().delete_queryset(request, queryset)
@admin.register(LoopTrigger)
class LoopTriggerAdmin(admin.ModelAdmin):
list_display = ("id", "loop", "type", "enabled", "schedule_sync_status", "last_fired_at")
list_filter = ("type", "enabled", "schedule_sync_status")
search_fields = ("loop__name",)
readonly_fields = ("id", "loop", "enabled", "schedule_sync_status", "last_fired_at", "created_at", "updated_at")
autocomplete_fields = ("team",)
def get_queryset(self, request: HttpRequest):
# Admin has no team context; select_related("loop") also keeps readonly
# rendering off Loop's fail-closed base manager.
return LoopTrigger.objects.unscoped().select_related("loop", "team")
def has_add_permission(self, request: HttpRequest) -> bool:
# Triggers are created through the loops API; their Temporal Schedule
# identity hangs off the row id, so hand-created rows would drift.
return False
def delete_model(self, request: HttpRequest, obj: LoopTrigger) -> None:
# Tear down the Temporal Schedule before the row is gone (CASCADE won't).
loop_service.delete_loop_trigger_schedule(obj)
super().delete_model(request, obj)
def delete_queryset(self, request: HttpRequest, queryset) -> None:
for trigger in queryset:
loop_service.delete_loop_trigger_schedule(trigger)
super().delete_queryset(request, queryset)