simpleaudit-studio 0.2.6__tar.gz → 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.gitignore +4 -0
  2. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/PKG-INFO +34 -1
  3. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/README.md +33 -0
  4. simpleaudit_studio-0.2.7/accounts/admin_urls.py +11 -0
  5. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/auth_urls.py +1 -0
  6. simpleaudit_studio-0.2.7/accounts/migrations/0003_project_archived.py +17 -0
  7. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/models.py +3 -0
  8. simpleaudit_studio-0.2.7/accounts/serializers.py +180 -0
  9. simpleaudit_studio-0.2.7/accounts/services.py +402 -0
  10. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/views.py +100 -8
  11. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/comparison.py +17 -17
  12. simpleaudit_studio-0.2.7/audits/migrations/0004_swap_endpoint_fks_for_models.py +111 -0
  13. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/models.py +3 -3
  14. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/serializers.py +3 -3
  15. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/services.py +31 -25
  16. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/views.py +11 -10
  17. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/settings.py +1 -0
  18. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/urls.py +20 -6
  19. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/e2e_smoke.py +13 -19
  20. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/seed_bulk.py +17 -16
  21. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docs/architecture.md +3 -2
  22. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docs/domain-model.md +33 -13
  23. simpleaudit_studio-0.2.7/docs/superpowers/specs/2026-09-26-super-admin-dashboard-design.md +254 -0
  24. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/e2e/test_ui.py +3 -3
  25. simpleaudit_studio-0.2.7/infra/context_processors.py +80 -0
  26. simpleaudit_studio-0.2.7/infra/management/commands/dev_server.py +116 -0
  27. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/purge_test_data.py +2 -2
  28. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/seed_demo_audits.py +14 -13
  29. simpleaudit_studio-0.2.7/infra/management/commands/setup_local.py +66 -0
  30. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/seed.py +2 -14
  31. simpleaudit_studio-0.2.7/infra/seo.py +195 -0
  32. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/factories.py +4 -14
  33. simpleaudit_studio-0.2.7/infra/tests/test_admin_api.py +173 -0
  34. simpleaudit_studio-0.2.7/infra/tests/test_admin_page.py +101 -0
  35. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_api_e2e.py +27 -19
  36. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_clone.py +15 -10
  37. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_poll_results.py +7 -6
  38. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_rename.py +2 -2
  39. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_run_freeze.py +34 -34
  40. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_sse.py +7 -6
  41. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_submission.py +16 -13
  42. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_bootstrap.py +5 -6
  43. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_comparison.py +14 -11
  44. simpleaudit_studio-0.2.7/infra/tests/test_connection_delete.py +109 -0
  45. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_engine_integration.py +14 -11
  46. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_health_panel.py +2 -3
  47. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_minimal_config.py +10 -8
  48. simpleaudit_studio-0.2.7/infra/tests/test_model_registry.py +47 -0
  49. simpleaudit_studio-0.2.7/infra/tests/test_profile.py +122 -0
  50. simpleaudit_studio-0.2.7/infra/tests/test_profile_ui.py +85 -0
  51. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_purge_test_data.py +21 -15
  52. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_scenarios.py +0 -12
  53. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_seed_platform.py +2 -5
  54. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_smoke_all_pages.py +25 -8
  55. simpleaudit_studio-0.2.7/infra/tests/test_workspace_archive.py +120 -0
  56. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_workspaces.py +20 -8
  57. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/ui.py +271 -63
  58. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/worker.py +117 -51
  59. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/admin.py +1 -8
  60. simpleaudit_studio-0.2.7/model_registry/migrations/0002_migrate_legacy_endpoints.py +53 -0
  61. simpleaudit_studio-0.2.7/model_registry/migrations/0003_remove_modeleendpoint.py +18 -0
  62. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/models.py +5 -33
  63. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/serializers.py +0 -32
  64. simpleaudit_studio-0.2.7/model_registry/services.py +3 -0
  65. simpleaudit_studio-0.2.7/model_registry/urls.py +7 -0
  66. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/views.py +1 -27
  67. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/pyproject.toml +1 -1
  68. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/services.py +3 -1
  69. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/views.py +2 -1
  70. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/simpleaudit_studio/cli.py +76 -8
  71. simpleaudit_studio-0.2.7/templates/admin.html +460 -0
  72. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/audit_detail.html +3 -3
  73. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/base.html +25 -8
  74. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/dashboard.html +3 -3
  75. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/new_audit.html +38 -8
  76. simpleaudit_studio-0.2.7/templates/profile.html +134 -0
  77. simpleaudit_studio-0.2.7/templates/public/base_public.html +20 -0
  78. simpleaudit_studio-0.2.7/templates/public/landing.html +99 -0
  79. simpleaudit_studio-0.2.7/templates/seo/head.html +24 -0
  80. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/workspaces.html +1 -0
  81. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/uv.lock +1 -1
  82. simpleaudit_studio-0.2.6/Makefile +0 -63
  83. simpleaudit_studio-0.2.6/accounts/serializers.py +0 -95
  84. simpleaudit_studio-0.2.6/accounts/services.py +0 -158
  85. simpleaudit_studio-0.2.6/infra/context_processors.py +0 -65
  86. simpleaudit_studio-0.2.6/infra/tests/test_model_registry.py +0 -89
  87. simpleaudit_studio-0.2.6/model_registry/services.py +0 -39
  88. simpleaudit_studio-0.2.6/model_registry/urls.py +0 -13
  89. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.dockerignore +0 -0
  90. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.env.example +0 -0
  91. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.env.local.example +0 -0
  92. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.github/workflows/ci.yml +0 -0
  93. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/.github/workflows/publish.yml +0 -0
  94. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/Dockerfile +0 -0
  95. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/LICENSE +0 -0
  96. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/__init__.py +0 -0
  97. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/admin.py +0 -0
  98. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/apps.py +0 -0
  99. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/migrations/0001_initial.py +0 -0
  100. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/migrations/0002_user_workos_user_id.py +0 -0
  101. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/migrations/__init__.py +0 -0
  102. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/project_urls.py +0 -0
  103. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/accounts/workos_auth.py +0 -0
  104. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/__init__.py +0 -0
  105. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/admin.py +0 -0
  106. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/apps.py +0 -0
  107. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/events.py +0 -0
  108. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/0001_initial.py +0 -0
  109. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/0002_add_audit_run_archived.py +0 -0
  110. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/0003_auditrun_updated_at.py +0 -0
  111. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/migrations/__init__.py +0 -0
  112. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/audits/urls.py +0 -0
  113. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/__init__.py +0 -0
  114. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/asgi.py +0 -0
  115. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/config/wsgi.py +0 -0
  116. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/compose/Dockerfile +0 -0
  117. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/mock_openai_server.py +0 -0
  118. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/deploy/postgres-init.sql +0 -0
  119. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docker-compose.yml +0 -0
  120. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/docs/deployment.md +0 -0
  121. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/__init__.py +0 -0
  122. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/admin.py +0 -0
  123. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/apps.py +0 -0
  124. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/engine.py +0 -0
  125. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/exceptions.py +0 -0
  126. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/fixtures/demo_audit_results.json +0 -0
  127. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/hashing.py +0 -0
  128. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/health.py +0 -0
  129. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/health_api.py +0 -0
  130. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/logging.py +0 -0
  131. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/__init__.py +0 -0
  132. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/__init__.py +0 -0
  133. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/bootstrap_platform.py +0 -0
  134. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/run_worker.py +0 -0
  135. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/management/commands/seed_platform.py +0 -0
  136. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/middleware.py +0 -0
  137. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/minimal_config.py +0 -0
  138. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/permissions.py +0 -0
  139. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/readiness.py +0 -0
  140. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/simpleaudit_package.py +0 -0
  141. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/startup_checks.py +0 -0
  142. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/__init__.py +0 -0
  143. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_archive.py +0 -0
  144. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_events.py +0 -0
  145. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_audit_timing.py +0 -0
  146. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_auth_projects.py +0 -0
  147. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_health.py +0 -0
  148. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_middleware.py +0 -0
  149. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_scenario_import_export.py +0 -0
  150. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/infra/tests/test_workspaces_ui.py +0 -0
  151. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/manage.py +0 -0
  152. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/__init__.py +0 -0
  153. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/apps.py +0 -0
  154. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/migrations/0001_initial.py +0 -0
  155. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/model_registry/migrations/__init__.py +0 -0
  156. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/__init__.py +0 -0
  157. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/admin.py +0 -0
  158. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/apps.py +0 -0
  159. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/migrations/0001_initial.py +0 -0
  160. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/migrations/__init__.py +0 -0
  161. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/models.py +0 -0
  162. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/serializers.py +0 -0
  163. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/scenarios/urls.py +0 -0
  164. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/simpleaudit_studio/__init__.py +0 -0
  165. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/favicon-16x16.svg +0 -0
  166. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/favicon-32x32.svg +0 -0
  167. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/favicon.svg +0 -0
  168. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/js/diff.min.js +0 -0
  169. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/logo.png +0 -0
  170. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/static/logo.svg +0 -0
  171. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/404.html +0 -0
  172. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/login.html +0 -0
  173. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/register.html +0 -0
  174. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/workos_login.html +0 -0
  175. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/auth/workos_verify.html +0 -0
  176. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/compare.html +0 -0
  177. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/health.html +0 -0
  178. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/models.html +0 -0
  179. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/scenario_result_detail.html +0 -0
  180. {simpleaudit_studio-0.2.6 → simpleaudit_studio-0.2.7}/templates/scenarios.html +0 -0
@@ -24,6 +24,7 @@ simpleaudit-dependency.yaml
24
24
  .idea/
25
25
  *.swp
26
26
  .DS_Store
27
+ copilot-instructions.md
27
28
 
28
29
  # Test/coverage
29
30
  .coverage
@@ -37,5 +38,8 @@ docker-data/
37
38
  *.log
38
39
  logs/
39
40
 
41
+ # Runtime artifacts
42
+ app.pid
43
+
40
44
  # Data artifacts (prototype)
41
45
  data/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simpleaudit-studio
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Production AI model audit platform — one-liner demo mode via uvx
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -80,6 +80,39 @@ The pre-seeded connections point at a built-in mock server. To run real audits,
80
80
 
81
81
  Set the API key in the connection form (leave blank for local servers that don't require auth), then launch an audit from the Scenario Library.
82
82
 
83
+ ## 🛠️ Local Development
84
+
85
+ For contributors working on the codebase, use [uv](https://docs.astral.sh/uv/) — no manual venv steps, no Makefile. Two verbs cover everything:
86
+
87
+ - **`uv sync`** — create/update `.venv` from `pyproject.toml` + `uv.lock`
88
+ - **`uv run <cmd>`** — run any command inside `.venv` (auto-syncs first if the lockfile changed)
89
+
90
+ ### Setup (one time)
91
+
92
+ ```bash
93
+ git clone https://github.com/SushantGautam/SimpleAuditStudio
94
+ cd SimpleAuditStudio
95
+ cp .env.local.example .env # minimal local config (SQLite + mock models)
96
+
97
+ uv sync --extra dev --extra postgres # create .venv + install all deps
98
+ uv run manage.py setup_local # migrate + create admin (studio/admin123) + seed data
99
+ ```
100
+
101
+ `setup_local` chains the three idempotent first-run steps (migrate → bootstrap admin → seed scenario packs & model connections) into one command. Re-run it any time — it skips what already exists.
102
+
103
+ ### Daily work
104
+
105
+ ```bash
106
+ uv run manage.py dev_server --embedded # easiest: web UI + API + audit worker, zero-Docker → http://localhost:8000
107
+ ```
108
+
109
+ Other options:
110
+ ```bash
111
+ uv run manage.py runserver # web UI + API only (no worker)
112
+ uv run manage.py dev_server # web UI + worker, uses .env (edit POSTGRES_*/HATCHET_* to point at your infra)
113
+ SIMPLEAUDIT_LOCAL_SQLITE=1 uv run manage.py test infra # tests
114
+ ```
115
+
83
116
  ## 🐳 Self-Hosting
84
117
 
85
118
  For teams or multi-user setups, use Docker Compose:
@@ -50,6 +50,39 @@ The pre-seeded connections point at a built-in mock server. To run real audits,
50
50
 
51
51
  Set the API key in the connection form (leave blank for local servers that don't require auth), then launch an audit from the Scenario Library.
52
52
 
53
+ ## 🛠️ Local Development
54
+
55
+ For contributors working on the codebase, use [uv](https://docs.astral.sh/uv/) — no manual venv steps, no Makefile. Two verbs cover everything:
56
+
57
+ - **`uv sync`** — create/update `.venv` from `pyproject.toml` + `uv.lock`
58
+ - **`uv run <cmd>`** — run any command inside `.venv` (auto-syncs first if the lockfile changed)
59
+
60
+ ### Setup (one time)
61
+
62
+ ```bash
63
+ git clone https://github.com/SushantGautam/SimpleAuditStudio
64
+ cd SimpleAuditStudio
65
+ cp .env.local.example .env # minimal local config (SQLite + mock models)
66
+
67
+ uv sync --extra dev --extra postgres # create .venv + install all deps
68
+ uv run manage.py setup_local # migrate + create admin (studio/admin123) + seed data
69
+ ```
70
+
71
+ `setup_local` chains the three idempotent first-run steps (migrate → bootstrap admin → seed scenario packs & model connections) into one command. Re-run it any time — it skips what already exists.
72
+
73
+ ### Daily work
74
+
75
+ ```bash
76
+ uv run manage.py dev_server --embedded # easiest: web UI + API + audit worker, zero-Docker → http://localhost:8000
77
+ ```
78
+
79
+ Other options:
80
+ ```bash
81
+ uv run manage.py runserver # web UI + API only (no worker)
82
+ uv run manage.py dev_server # web UI + worker, uses .env (edit POSTGRES_*/HATCHET_* to point at your infra)
83
+ SIMPLEAUDIT_LOCAL_SQLITE=1 uv run manage.py test infra # tests
84
+ ```
85
+
53
86
  ## 🐳 Self-Hosting
54
87
 
55
88
  For teams or multi-user setups, use Docker Compose:
@@ -0,0 +1,11 @@
1
+ from django.urls import path
2
+
3
+ from . import views
4
+
5
+ urlpatterns = [
6
+ path("stats/", views.admin_stats, name="admin-stats"),
7
+ path("workspaces/<int:project_id>/archive/", views.admin_archive_workspace, name="admin-workspace-archive"),
8
+ path("workspaces/<int:project_id>/unarchive/", views.admin_unarchive_workspace, name="admin-workspace-unarchive"),
9
+ path("users/", views.admin_create_user_view, name="admin-user-create"),
10
+ path("users/<int:user_id>/", views.admin_update_user_view, name="admin-user-update"),
11
+ ]
@@ -6,4 +6,5 @@ urlpatterns = [
6
6
  path("register/", views.register, name="auth-register"),
7
7
  path("token/", views.obtain_token, name="auth-token"),
8
8
  path("me/", views.me, name="auth-me"),
9
+ path("profile/", views.update_profile, name="auth-profile"),
9
10
  ]
@@ -0,0 +1,17 @@
1
+ # Generated by Django 5.2.4 on 2026-09-26 08:58
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("accounts", "0002_user_workos_user_id"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="project",
14
+ name="archived",
15
+ field=models.BooleanField(db_index=True, default=False),
16
+ ),
17
+ ]
@@ -45,6 +45,9 @@ class Project(models.Model):
45
45
  name = models.CharField(max_length=200, unique=True)
46
46
  slug = models.SlugField(max_length=200, unique=True)
47
47
  description = models.TextField(blank=True)
48
+ # Soft-hide a workspace that still contains data. Members keep read access;
49
+ # all mutations are blocked for non-superusers. Never deletes data.
50
+ archived = models.BooleanField(default=False, db_index=True)
48
51
  created_at = models.DateTimeField(auto_now_add=True)
49
52
  updated_at = models.DateTimeField(auto_now=True)
50
53
 
@@ -0,0 +1,180 @@
1
+ from django.contrib.auth.password_validation import validate_password
2
+ from rest_framework import serializers
3
+
4
+ from accounts.models import Project, ProjectMembership, User
5
+
6
+
7
+ class UserSerializer(serializers.ModelSerializer):
8
+ class Meta:
9
+ model = User
10
+ fields = ("id", "username", "email", "first_name", "last_name", "is_active")
11
+ read_only_fields = ("id",)
12
+
13
+
14
+ class RegisterSerializer(serializers.Serializer):
15
+ username = serializers.CharField(max_length=150)
16
+ email = serializers.EmailField()
17
+ password = serializers.CharField(write_only=True, style={"input_type": "password"})
18
+ first_name = serializers.CharField(required=False, allow_blank=True, default="")
19
+ last_name = serializers.CharField(required=False, allow_blank=True, default="")
20
+
21
+ def validate_username(self, value):
22
+ if User.objects.filter(username__iexact=value).exists():
23
+ raise serializers.ValidationError("Username already exists.")
24
+ return value
25
+
26
+ def validate_email(self, value):
27
+ if User.objects.filter(email__iexact=value).exists():
28
+ raise serializers.ValidationError("Email already exists.")
29
+ return value
30
+
31
+ def validate_password(self, value):
32
+ validate_password(value)
33
+ return value
34
+
35
+ def create(self, validated_data):
36
+ return User.objects.create_user(**validated_data)
37
+
38
+
39
+ class ProjectSerializer(serializers.ModelSerializer):
40
+ class Meta:
41
+ model = Project
42
+ fields = ("id", "name", "slug", "description", "created_at", "updated_at")
43
+ read_only_fields = ("id", "created_at", "updated_at")
44
+
45
+
46
+ class WorkspaceItemSerializer(serializers.ModelSerializer):
47
+ """Workspace as shown in lists/detail, including whether the requesting
48
+ user can administer it (drives UI affordances)."""
49
+
50
+ is_admin = serializers.SerializerMethodField()
51
+
52
+ class Meta:
53
+ model = Project
54
+ fields = ("id", "name", "slug", "description", "is_admin", "archived", "created_at", "updated_at")
55
+ read_only_fields = fields
56
+
57
+ def get_is_admin(self, obj) -> bool:
58
+ request = self.context.get("request")
59
+ user = getattr(request, "user", None)
60
+ if user is None or not user.is_authenticated:
61
+ return False
62
+ if user.is_superuser:
63
+ return True
64
+ return ProjectMembership.objects.filter(
65
+ project=obj, user=user, role=ProjectMembership.Role.ADMIN
66
+ ).exists()
67
+
68
+
69
+ class WorkspaceCreateSerializer(serializers.Serializer):
70
+ name = serializers.CharField(max_length=200)
71
+ description = serializers.CharField(required=False, allow_blank=True, default="")
72
+
73
+
74
+ class WorkspaceUpdateSerializer(serializers.Serializer):
75
+ name = serializers.CharField(max_length=200, required=False)
76
+ description = serializers.CharField(required=False, allow_blank=True)
77
+
78
+
79
+ class MemberAddSerializer(serializers.Serializer):
80
+ username = serializers.CharField(max_length=150)
81
+ role = serializers.ChoiceField(choices=ProjectMembership.Role.choices, default=ProjectMembership.Role.VIEWER)
82
+
83
+
84
+ class MemberRoleSerializer(serializers.Serializer):
85
+ role = serializers.ChoiceField(choices=ProjectMembership.Role.choices)
86
+
87
+
88
+ # ─── Profile (self-service) ──────────────────────────────────────────────────
89
+
90
+
91
+ class ProfileUpdateSerializer(serializers.Serializer):
92
+ """Self-service profile update. All fields optional; only provided fields
93
+ are changed. Password change requires the current password."""
94
+
95
+ first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
96
+ last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
97
+ email = serializers.EmailField(required=False, allow_blank=True)
98
+ username = serializers.CharField(required=False, max_length=150)
99
+ current_password = serializers.CharField(required=False, style={"input_type": "password"}, write_only=True)
100
+ new_password = serializers.CharField(required=False, style={"input_type": "password"}, write_only=True)
101
+
102
+ def validate_username(self, value):
103
+ value = (value or "").strip()
104
+ if not value:
105
+ raise serializers.ValidationError("Username cannot be empty.")
106
+ if User.objects.filter(username__iexact=value).exclude(pk=self.context["user"].pk).exists():
107
+ raise serializers.ValidationError("Username already exists.")
108
+ return value
109
+
110
+ def validate_email(self, value):
111
+ value = (value or "").strip()
112
+ if value and User.objects.filter(email__iexact=value).exclude(pk=self.context["user"].pk).exists():
113
+ raise serializers.ValidationError("Email already exists.")
114
+ return value
115
+
116
+ def validate(self, attrs):
117
+ from accounts.services import has_local_password
118
+
119
+ user = self.context["user"]
120
+ if "new_password" in attrs:
121
+ if not attrs.get("new_password"):
122
+ raise serializers.ValidationError({"new_password": "Enter a new password."})
123
+ # SSO users (and anyone without a local password) have nothing to
124
+ # verify against, so they set a password directly.
125
+ if has_local_password(user):
126
+ if not attrs.get("current_password"):
127
+ raise serializers.ValidationError({"current_password": "Current password is required."})
128
+ if not user.check_password(attrs["current_password"]):
129
+ raise serializers.ValidationError({"current_password": "Current password is incorrect."})
130
+ validate_password(attrs["new_password"])
131
+ return attrs
132
+
133
+ def save(self):
134
+ user = self.context["user"]
135
+ attrs = self.validated_data
136
+ if "first_name" in attrs:
137
+ user.first_name = attrs["first_name"]
138
+ if "last_name" in attrs:
139
+ user.last_name = attrs["last_name"]
140
+ if "email" in attrs:
141
+ user.email = attrs["email"]
142
+ if "username" in attrs:
143
+ user.username = attrs["username"]
144
+ user.save()
145
+ if attrs.get("new_password"):
146
+ user.set_password(attrs["new_password"])
147
+ user.save(update_fields=["password"])
148
+ return user
149
+
150
+
151
+ # ─── Super-admin user management ─────────────────────────────────────────────
152
+
153
+
154
+ class UserCreateSerializer(serializers.Serializer):
155
+ username = serializers.CharField(max_length=150)
156
+ email = serializers.EmailField(required=False, allow_blank=True)
157
+ password = serializers.CharField(write_only=True, style={"input_type": "password"})
158
+ first_name = serializers.CharField(required=False, allow_blank=True, default="")
159
+ last_name = serializers.CharField(required=False, allow_blank=True, default="")
160
+
161
+
162
+ class UserAdminUpdateSerializer(serializers.Serializer):
163
+ """Super-admin edit of any user. All fields optional."""
164
+
165
+ first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
166
+ last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
167
+ email = serializers.EmailField(required=False, allow_blank=True)
168
+ is_active = serializers.BooleanField(required=False)
169
+ is_superuser = serializers.BooleanField(required=False)
170
+ password = serializers.CharField(required=False, write_only=True, style={"input_type": "password"})
171
+
172
+
173
+ class ProjectMembershipSerializer(serializers.ModelSerializer):
174
+ username = serializers.CharField(source="user.username", read_only=True)
175
+ email = serializers.EmailField(source="user.email", read_only=True)
176
+
177
+ class Meta:
178
+ model = ProjectMembership
179
+ fields = ("id", "project", "user", "username", "email", "role", "created_at")
180
+ read_only_fields = ("id", "created_at")