Skip to content

Commit cf8dd31

Browse files
committed
Rename Tenant to Project
1 parent 92ebb04 commit cf8dd31

43 files changed

Lines changed: 939 additions & 934 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
An AI-powered content curation platform for technically-oriented newsletters. Newsletter Maker ingests content from dozens of sources, builds authority models of people and companies in a domain, and surfaces the most relevant articles, trends, and themes for each edition — so editors spend their time writing, not searching.
66

7-
The system is multi-tenant: each newsletter has its own tracked entities, relevance model, and content pipeline. Designed for non-technical editors who don't know what a vector database is and don't need to.
7+
The system is organized into projects: each newsletter project has its own tracked entities, relevance model, and content pipeline. Projects are assigned to Django groups so editorial access can be shared cleanly. Designed for non-technical editors who don't know what a vector database is and don't need to.
88

99
## What This Does That Existing Tools Don't
1010

1111
Tools like Feedly, UpContent, and ContentStudio handle parts of the content curation problem. Newsletter Maker combines several capabilities none of them offer:
1212

1313
- **Authority scoring from newsletter cross-referencing.** By ingesting peer newsletters, the system builds an authority model based on who real editors actually link to — a human-curated endorsement signal no existing tool provides.
14-
- **Per-editor relevance training.** Upvote/downvote feedback trains a personalized relevance model per tenant. The tool learns what *you* consider valuable.
14+
- **Per-project relevance training.** Upvote/downvote feedback trains a personalized relevance model per project. The tool learns what each editorial project considers valuable.
1515
- **Unified entity model.** A person's blog, LinkedIn, Bluesky, GitHub, and conference talks are linked into a single profile with an authority score — a holistic view of who matters in a space.
1616
- **Competitive intelligence.** "These 3 peer newsletters all covered topic X this week, but you haven't." A natural output of newsletter ingestion that no curation tool provides.
1717
- **Historical trend analysis.** Not just what's trending now, but trajectories over weeks. Content is retained indefinitely for long-term pattern detection.
@@ -102,8 +102,9 @@ python3 -m pip install -r requirements.txt
102102

103103
1. Run `just dev` to start Django, Celery, Postgres, Redis, Qdrant, and Nginx. On the first run Docker builds the app image automatically. After that, `just dev` reuses the existing image so normal restarts are fast. If `.env` is missing, the `just` command copies `.env.example` automatically.
104104
2. Run `just build` after changing `requirements.txt` or `docker/web/Dockerfile`.
105-
3. Update `.env` with non-default secrets before using the stack outside local development. The example file uses SQLite and localhost URLs so host-side `manage.py` commands work even without Docker.
106-
4. Open `http://localhost:8080/healthz/` for a liveness check and `http://localhost:8080/admin/` for Django admin.
105+
3. For a fully fresh local stack after schema changes, run `just reset-volumes` before starting the containers again. This drops the Docker-backed Postgres, Redis, and Qdrant state so regenerated migrations apply cleanly.
106+
4. Update `.env` with non-default secrets before using the stack outside local development. The example file uses SQLite and localhost URLs so host-side `manage.py` commands work even without Docker.
107+
5. Open `http://localhost:8080/healthz/` for a liveness check and `http://localhost:8080/admin/` for Django admin. Use `just seed` after the stack is up if you want the demo project and sample content.
107108

108109
For host-based development without Docker, install `requirements.txt`, then use `python3 manage.py migrate` and `python3 manage.py runserver`. The default `.env.example` is host-safe; Docker Compose overrides the service URLs inside containers.
109110

@@ -155,7 +156,7 @@ Use these commands to backfill or refresh embeddings for existing content:
155156

156157
```bash
157158
just embed-all
158-
just embed-tenant 1
159+
just embed-project 1
159160
python3 manage.py sync_embeddings --content-id 42
160161
python3 manage.py sync_embeddings --references-only
161162
```

core/admin.py

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,60 +12,60 @@
1212
Content,
1313
Entity,
1414
IngestionRun,
15+
Project,
16+
ProjectConfig,
1517
ReviewQueue,
1618
SkillResult,
1719
SourceConfig,
18-
Tenant,
19-
TenantConfig,
2020
UserFeedback,
2121
)
2222
from core.plugins import get_plugin_for_source_config, validate_plugin_config
2323
from core.tasks import process_content
2424

2525

26-
@admin.register(Tenant)
27-
class TenantAdmin(ExportActionMixin, admin.ModelAdmin):
28-
list_display = ("name", "user", "content_retention_days", "created_at")
26+
@admin.register(Project)
27+
class ProjectAdmin(ExportActionMixin, admin.ModelAdmin):
28+
list_display = ("name", "group", "content_retention_days", "created_at")
2929

30-
# Better navigation
31-
date_hierarchy = "created_at"
32-
list_filter = ("created_at",)
30+
# Better navigation
31+
date_hierarchy = "created_at"
32+
list_filter = ("created_at",)
3333

34-
# Faster searching
35-
search_fields = ("name", "user__username", "user__email")
34+
# Faster searching
35+
search_fields = ("name", "group__name")
3636

37-
# Performance for large user lists
38-
autocomplete_fields = ("user",)
37+
# Performance for large user lists
38+
autocomplete_fields = ("group",)
3939

40-
# Quick editing
41-
list_editable = ("content_retention_days",)
40+
# Quick editing
41+
list_editable = ("content_retention_days",)
4242

4343

44-
@admin.register(TenantConfig)
45-
class TenantConfigAdmin(admin.ModelAdmin):
46-
list_display = ("tenant", "upvote_authority_weight", "downvote_authority_weight", "authority_decay_rate")
44+
@admin.register(ProjectConfig)
45+
class ProjectConfigAdmin(admin.ModelAdmin):
46+
list_display = ("project", "upvote_authority_weight", "downvote_authority_weight", "authority_decay_rate")
4747

4848

4949
@admin.register(Entity)
5050
class EntityAdmin(admin.ModelAdmin):
51-
# Replace 'authority_score' with your new method name
52-
list_display = ("name", "tenant", "type", "colored_score", "created_at")
53-
54-
@admin.display(description="Authority Score", ordering="authority_score")
55-
def colored_score(self, obj):
56-
# Choose a color based on the value
57-
if obj.authority_score >= 80:
58-
color = "green"
59-
elif obj.authority_score >= 50:
60-
color = "orange"
61-
else:
62-
color = "red"
63-
64-
return format_html(
65-
'<span style="color: {}; font-weight: bold;">{}</span>',
66-
color,
67-
obj.authority_score,
68-
)
51+
# Replace 'authority_score' with your new method name
52+
list_display = ("name", "project", "type", "colored_score", "created_at")
53+
54+
@admin.display(description="Authority Score", ordering="authority_score")
55+
def colored_score(self, obj):
56+
# Choose a color based on the value
57+
if obj.authority_score >= 80:
58+
color = "green"
59+
elif obj.authority_score >= 50:
60+
color = "orange"
61+
else:
62+
color = "red"
63+
64+
return format_html(
65+
'<span style="color: {}; font-weight: bold;">{}</span>',
66+
color,
67+
obj.authority_score,
68+
)
6969

7070

7171
class HighValueFilter(admin.SimpleListFilter):
@@ -91,14 +91,14 @@ class ContentAdmin(admin.ModelAdmin):
9191
"is_reference",
9292
"preview_content",
9393
"source_plugin",
94-
"tenant",
94+
"project",
9595
"title",
9696
"view_trace",
9797
)
9898
list_editable = ("is_reference", "is_active")
9999
list_filter = (
100100
HighValueFilter,
101-
("tenant", admin.RelatedOnlyFieldListFilter),
101+
("project", admin.RelatedOnlyFieldListFilter),
102102
"source_plugin",
103103
"is_active"
104104
)
@@ -159,7 +159,7 @@ def view_trace(self, obj):
159159
run_id=trace_id,
160160
skill_name=latest_skill_result.skill_name,
161161
skill_result_id=latest_skill_result.id,
162-
tenant_id=obj.tenant_id,
162+
project_id=obj.project_id,
163163
trace_id=trace_id,
164164
)
165165

@@ -233,13 +233,13 @@ class SkillResultAdmin(ModelAdmin):
233233
"model_used",
234234
"created_at",
235235
)
236-
list_filter = ("status", "skill_name", "tenant", "model_used")
236+
list_filter = ("status", "skill_name", "project", "model_used")
237237
search_fields = ("skill_name", "content__title", "model_used", "error_message")
238238
actions = ["retry_selected_skills"]
239239
readonly_fields = ("pretty_result_data", "latency_ms", "created_at", "superseded_by")
240240
fieldsets = (
241241
("Execution Details", {
242-
"fields": ("skill_name", "content", "tenant", "status", "model_used")
242+
"fields": ("skill_name", "content", "project", "status", "model_used")
243243
}),
244244
("AI Output", {
245245
"fields": ("pretty_result_data", "error_message"),
@@ -334,11 +334,11 @@ class UserFeedbackAdmin(ModelAdmin):
334334
"display_feedback",
335335
"get_content_title",
336336
"get_ai_score",
337-
"tenant",
337+
"project",
338338
"user",
339339
"created_at"
340340
)
341-
list_filter = ("feedback_type", ("tenant", admin.RelatedOnlyFieldListFilter))
341+
list_filter = ("feedback_type", ("project", admin.RelatedOnlyFieldListFilter))
342342
search_fields = ("content__title", "user__email", "user__username")
343343

344344
@admin.display(description="Type")
@@ -387,18 +387,18 @@ def changelist_view(self, request, extra_context=None):
387387
class IngestionRunAdmin(ModelAdmin):
388388
list_display = (
389389
"plugin_name",
390-
"tenant",
390+
"project",
391391
"display_status",
392392
"display_efficiency",
393393
"display_duration",
394394
"started_at",
395395
)
396-
list_filter = ("plugin_name", "status", ("tenant", admin.RelatedOnlyFieldListFilter))
397-
search_fields = ("plugin_name", "error_message", "tenant__name")
396+
list_filter = ("plugin_name", "status", ("project", admin.RelatedOnlyFieldListFilter))
397+
search_fields = ("plugin_name", "error_message", "project__name")
398398
readonly_fields = ("display_duration", "started_at", "completed_at")
399399
fieldsets = (
400400
("Run Info", {
401-
"fields": ("plugin_name", "tenant", "status")
401+
"fields": ("plugin_name", "project", "status")
402402
}),
403403
("Data Metrics", {
404404
"fields": ("items_fetched", "items_ingested", "display_efficiency")
@@ -468,19 +468,19 @@ def changelist_view(self, request, extra_context=None):
468468
class SourceConfigAdmin(ModelAdmin):
469469
list_display = (
470470
"plugin_name",
471-
"tenant",
471+
"project",
472472
"display_health",
473473
"is_active",
474474
"last_fetched_at",
475475
)
476-
list_filter = ("is_active", "plugin_name", ("tenant", admin.RelatedOnlyFieldListFilter))
476+
list_filter = ("is_active", "plugin_name", ("project", admin.RelatedOnlyFieldListFilter))
477477
list_editable = ("is_active",)
478-
search_fields = ("plugin_name", "tenant__name")
478+
search_fields = ("plugin_name", "project__name")
479479
actions = ["test_source_connection"]
480480
readonly_fields = ("last_fetched_at", "pretty_config")
481481
fieldsets = (
482482
("Core Settings", {
483-
"fields": ("plugin_name", "tenant", "is_active")
483+
"fields": ("plugin_name", "project", "is_active")
484484
}),
485485
("Configuration", {
486486
"fields": ("pretty_config", "config"),
@@ -519,7 +519,7 @@ def test_source_connection(self, request, queryset):
519519
healthy_sources = []
520520
failed_sources = []
521521

522-
for source_config in queryset.select_related("tenant"):
522+
for source_config in queryset.select_related("project"):
523523
try:
524524
source_config.config = validate_plugin_config(
525525
source_config.plugin_name,
@@ -573,14 +573,14 @@ def changelist_view(self, request, extra_context=None):
573573
class ReviewQueueAdmin(ModelAdmin):
574574
list_display = (
575575
"get_content_title",
576-
"tenant",
576+
"project",
577577
"reason",
578578
"display_confidence",
579579
"resolved",
580580
"resolution",
581581
"created_at",
582582
)
583-
list_filter = ("resolved", "reason", ("tenant", admin.RelatedOnlyFieldListFilter))
583+
list_filter = ("resolved", "reason", ("project", admin.RelatedOnlyFieldListFilter))
584584
list_editable = ("resolved", "resolution")
585585
actions = ["mark_as_approved", "mark_as_rejected"]
586586

0 commit comments

Comments
 (0)