teamtrack-tt 0.1.67__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 (71) hide show
  1. teamtrack_tt-0.1.67/MANIFEST.in +4 -0
  2. teamtrack_tt-0.1.67/PKG-INFO +390 -0
  3. teamtrack_tt-0.1.67/README.md +370 -0
  4. teamtrack_tt-0.1.67/pyproject.toml +64 -0
  5. teamtrack_tt-0.1.67/requirements.txt +21 -0
  6. teamtrack_tt-0.1.67/setup.cfg +4 -0
  7. teamtrack_tt-0.1.67/teamtrack/__init__.py +3 -0
  8. teamtrack_tt-0.1.67/teamtrack/agent.py +415 -0
  9. teamtrack_tt-0.1.67/teamtrack/assistant.py +401 -0
  10. teamtrack_tt-0.1.67/teamtrack/channels.py +344 -0
  11. teamtrack_tt-0.1.67/teamtrack/cli.py +887 -0
  12. teamtrack_tt-0.1.67/teamtrack/community.py +1093 -0
  13. teamtrack_tt-0.1.67/teamtrack/components.json +7 -0
  14. teamtrack_tt-0.1.67/teamtrack/config.py +95 -0
  15. teamtrack_tt-0.1.67/teamtrack/connect_mcp.py +239 -0
  16. teamtrack_tt-0.1.67/teamtrack/crypto.py +373 -0
  17. teamtrack_tt-0.1.67/teamtrack/deps.py +361 -0
  18. teamtrack_tt-0.1.67/teamtrack/hooks_progress.py +122 -0
  19. teamtrack_tt-0.1.67/teamtrack/hosting.py +456 -0
  20. teamtrack_tt-0.1.67/teamtrack/identity.py +286 -0
  21. teamtrack_tt-0.1.67/teamtrack/jira.py +1101 -0
  22. teamtrack_tt-0.1.67/teamtrack/lint.py +446 -0
  23. teamtrack_tt-0.1.67/teamtrack/mcp_bridge.py +439 -0
  24. teamtrack_tt-0.1.67/teamtrack/mirror.py +282 -0
  25. teamtrack_tt-0.1.67/teamtrack/sessions.py +189 -0
  26. teamtrack_tt-0.1.67/teamtrack/static/app.css +3549 -0
  27. teamtrack_tt-0.1.67/teamtrack/static/app.js +10985 -0
  28. teamtrack_tt-0.1.67/teamtrack/static/hero-eye-a.jpg +0 -0
  29. teamtrack_tt-0.1.67/teamtrack/static/hero-eye-b.jpg +0 -0
  30. teamtrack_tt-0.1.67/teamtrack/static/hero-eye-c.jpg +0 -0
  31. teamtrack_tt-0.1.67/teamtrack/static/hero-face.jpg +0 -0
  32. teamtrack_tt-0.1.67/teamtrack/static/hero-turn.jpg +0 -0
  33. teamtrack_tt-0.1.67/teamtrack/static/hero-walk.jpg +0 -0
  34. teamtrack_tt-0.1.67/teamtrack/static/index.html +515 -0
  35. teamtrack_tt-0.1.67/teamtrack/static/splash-hero.js +2194 -0
  36. teamtrack_tt-0.1.67/teamtrack/static/splash-strike.js +668 -0
  37. teamtrack_tt-0.1.67/teamtrack/static/splash-titlecard.js +772 -0
  38. teamtrack_tt-0.1.67/teamtrack/static/splash.js +1298 -0
  39. teamtrack_tt-0.1.67/teamtrack/volume.py +1652 -0
  40. teamtrack_tt-0.1.67/teamtrack_tt.egg-info/PKG-INFO +390 -0
  41. teamtrack_tt-0.1.67/teamtrack_tt.egg-info/SOURCES.txt +69 -0
  42. teamtrack_tt-0.1.67/teamtrack_tt.egg-info/dependency_links.txt +1 -0
  43. teamtrack_tt-0.1.67/teamtrack_tt.egg-info/entry_points.txt +5 -0
  44. teamtrack_tt-0.1.67/teamtrack_tt.egg-info/requires.txt +13 -0
  45. teamtrack_tt-0.1.67/teamtrack_tt.egg-info/top_level.txt +1 -0
  46. teamtrack_tt-0.1.67/tests/test_agent.py +243 -0
  47. teamtrack_tt-0.1.67/tests/test_assistant.py +362 -0
  48. teamtrack_tt-0.1.67/tests/test_channels.py +266 -0
  49. teamtrack_tt-0.1.67/tests/test_client_assets.py +702 -0
  50. teamtrack_tt-0.1.67/tests/test_community.py +510 -0
  51. teamtrack_tt-0.1.67/tests/test_connect_mcp.py +221 -0
  52. teamtrack_tt-0.1.67/tests/test_crypto.py +169 -0
  53. teamtrack_tt-0.1.67/tests/test_facet_cache.py +120 -0
  54. teamtrack_tt-0.1.67/tests/test_hooks_progress.py +146 -0
  55. teamtrack_tt-0.1.67/tests/test_hosting.py +218 -0
  56. teamtrack_tt-0.1.67/tests/test_identity.py +243 -0
  57. teamtrack_tt-0.1.67/tests/test_jira.py +313 -0
  58. teamtrack_tt-0.1.67/tests/test_logging.py +198 -0
  59. teamtrack_tt-0.1.67/tests/test_mcp_bridge.py +219 -0
  60. teamtrack_tt-0.1.67/tests/test_mirror.py +109 -0
  61. teamtrack_tt-0.1.67/tests/test_packaging.py +144 -0
  62. teamtrack_tt-0.1.67/tests/test_rephrase.py +230 -0
  63. teamtrack_tt-0.1.67/tests/test_rollup.py +330 -0
  64. teamtrack_tt-0.1.67/tests/test_route_table.py +294 -0
  65. teamtrack_tt-0.1.67/tests/test_routes.py +1611 -0
  66. teamtrack_tt-0.1.67/tests/test_routes_agent.py +330 -0
  67. teamtrack_tt-0.1.67/tests/test_sessions.py +328 -0
  68. teamtrack_tt-0.1.67/tests/test_settings.py +230 -0
  69. teamtrack_tt-0.1.67/tests/test_signals.py +160 -0
  70. teamtrack_tt-0.1.67/tests/test_static_cache.py +156 -0
  71. teamtrack_tt-0.1.67/tests/test_volume.py +469 -0
@@ -0,0 +1,4 @@
1
+ recursive-include teamtrack/static *
2
+ include teamtrack/components.json
3
+ include README.md
4
+ include requirements.txt
@@ -0,0 +1,390 @@
1
+ Metadata-Version: 2.4
2
+ Name: teamtrack-tt
3
+ Version: 0.1.67
4
+ Summary: Web UI for filing and tracking team requests, stored as encrypted Parquet
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/yogasathyandrun/team
7
+ Requires-Python: <3.12,>=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: fastapi>=0.110
10
+ Requires-Dist: uvicorn>=0.27
11
+ Requires-Dist: requests>=2.31
12
+ Requires-Dist: pyarrow>=15.0
13
+ Requires-Dist: cryptography>=42.0
14
+ Requires-Dist: databricks-sdk>=0.29
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=8.0; extra == "test"
17
+ Requires-Dist: httpx>=0.27; extra == "test"
18
+ Provides-Extra: laptop
19
+ Requires-Dist: keyring>=24.0; extra == "laptop"
20
+
21
+ # teamtrack
22
+
23
+ A team request tracker that runs on your laptop and stores everything in your own
24
+ private GitHub repos. No database, no server to host, no third-party service, no cost.
25
+
26
+ ```
27
+ pip install teamtrack-tt
28
+ tt
29
+ ```
30
+
31
+ `tt` starts a local server on `127.0.0.1`, opens your browser, and asks you to sign in
32
+ with GitHub. You never create an account, never paste a token, and never run `git`.
33
+
34
+ ---
35
+
36
+ ## How it works
37
+
38
+ | Piece | Choice | Why |
39
+ |---|---|---|
40
+ | Auth | OAuth **device flow** | No PAT to paste, no client secret to leak. The client ID is public by design. |
41
+ | Token | OS keychain via `keyring` | Never on disk in plaintext, never sent to the browser. |
42
+ | Writes | GitHub **Contents API** | No git binary, no clone, no working copy. |
43
+ | Projects | One private repo each, named `tt-<project>` | The repo is the boundary GitHub enforces. |
44
+ | Teams | A folder inside that repo | Nothing after the first repo creates another. Grouping the app enforces; GitHub still grants access per repo. |
45
+ | Concurrency | Append-only writes | Every write creates a new file. Two people filing at the same moment touch different paths and cannot conflict. |
46
+
47
+ The browser only ever talks to localhost. This Python process makes every GitHub call.
48
+
49
+ ### Storage layout
50
+
51
+ ```
52
+ teams/data-platform/team.json members + manager login digest
53
+ teams/data-platform/components.json that team's dependency map
54
+ teams/data-platform/data/requests/REQ-20260803-1420-A7F3.json
55
+ teams/data-platform/data/events/REQ-20260803-1420-A7F3/
56
+ 20260803T145500-acknowledged.json a status change is a new file
57
+ 20260803T151200-comment.json so is a comment
58
+ 20260804T091200-resolved.json
59
+ teams/data-platform/data/archive/2026-07.parquet cold archive, `tt compact`
60
+ ```
61
+
62
+ A manager creates a project and gets a private repo named `tt-<project>`, plus a
63
+ project login: the **project name is the username**, and the password they set is
64
+ stored only as a digest. Pasting an **existing repo link** into the create form uses
65
+ that repo as-is instead of making a new one — any of `https://github.com/owner/repo`,
66
+ the `.git` or SSH form, a deep link, or plain `owner/repo`. It needs admin on that
67
+ repo, refuses one that is already a project, and tags it with the `teamtrack` topic so
68
+ it is still listed when its name lacks the prefix. If the link points at a repo that
69
+ does not exist, it is created — but only on your own account. Requests land at the repo root. Team folders
70
+ (`teams/<slug>/`) remain available for subdividing a project but stay out of the
71
+ default path. Requests filed without a team sit at the repo
72
+ root (`data/requests/…`), offered in the UI as *(shared root)*.
73
+
74
+ Status is not a field on the request. It is derived by replaying the event files, which
75
+ is what lets concurrent writers never step on each other. Request ids
76
+ (`REQ-<date>-<time>-<4 random>`) are unique and sortable with zero coordination —
77
+ deliberately not sequential, because a counter would need shared mutable state.
78
+
79
+ ---
80
+
81
+ ## Setup
82
+
83
+ Nothing, for the common case. The package ships a working OAuth client ID
84
+ (`Ov23licr0i2DMXOl9CDq`) with device flow enabled, so `pip install teamtrack-tt && tt` signs
85
+ in as-is. Device flow issues no client secret, which is why that ID is safe to
86
+ publish — it identifies the app, it does not authorise anything on its own.
87
+
88
+ To point it at your own OAuth App instead (Settings → Developer settings → OAuth
89
+ Apps → New, then enable **device flow**):
90
+
91
+ ```sh
92
+ export TT_CLIENT_ID=Ov23li...
93
+ ```
94
+
95
+ ### Publishing
96
+
97
+ ```sh
98
+ python -m build && twine upload dist/*
99
+ ```
100
+
101
+ The published package is public and holds no secrets — no token, no password, no
102
+ client secret. Each teammate needs a GitHub account and has to accept the repo
103
+ invitation.
104
+
105
+ ---
106
+
107
+ ## Hosting it for your team
108
+
109
+ `tt` on a laptop and teamtrack behind a hostname are the same code with two
110
+ different threat models. Setting `TT_HOSTED=1` switches models; everything that
111
+ differs between them lives in [`teamtrack/hosting.py`](teamtrack/hosting.py).
112
+
113
+ | | laptop | hosted |
114
+ |---|---|---|
115
+ | Who the caller is | the one person at the keyboard | whoever the Databricks proxy names, per request |
116
+ | Credentials | this machine's OS keychain | environment variables, nothing on disk |
117
+ | `keyring` | the `[laptop]` extra | not installed at all |
118
+ | Manager password | the published default is fine | refuses to start with it |
119
+ | Jira | per machine, via `tt set jira` | one instance-wide service account, from env |
120
+
121
+ The keychain distinction is the whole point. On a laptop, "the stored credential" and
122
+ "the person using the app" are the same thing. On a server they are not — the first
123
+ person to set one up would leave it where every later request picked it up, and a
124
+ stranger loading the page would be served **as them**. So nothing in hosted mode reads
125
+ the keychain: there is no ambient identity on a server, only the one attached to the
126
+ request being served. That is also why `keyring` is an optional dependency — a
127
+ container can never reach the code that uses it.
128
+
129
+ ### Required
130
+
131
+ ```sh
132
+ TT_HOSTED=1 # implied when the platform sets PORT
133
+ TT_MANAGER_USER=someone # replaces the published manager1/m@n@ger
134
+ TT_MANAGER_PASS=<long password> # both required, or it refuses to boot
135
+ ```
136
+
137
+ It **refuses to start** while `manager1` / `m@n@ger` is still accepted. That
138
+ credential ships in a public package and every privileged route accepts it, so on a
139
+ public URL anyone who found the hostname could take the manager view and delete
140
+ requests. Setting the two variables above replaces the default rather than adding to
141
+ it.
142
+
143
+ ### Worth setting
144
+
145
+ ```sh
146
+ TT_SECRET=<long random string> # or each worker invents its own launch token
147
+ TT_ORIGINS=https://your.host # explicit; same-origin is matched anyway
148
+ TT_SESSION_HOURS=12 # session lifetime, default 12
149
+ TT_JIRA_SITE=you.atlassian.net # Jira, if wanted — see below
150
+ TT_JIRA_EMAIL=svc@you.com
151
+ TT_JIRA_TOKEN=<api token>
152
+ ```
153
+
154
+ ### Running it
155
+
156
+ ```sh
157
+ docker build -t teamtrack . && docker run -e PORT=7717 -p 7717:7717 \
158
+ -e TT_MANAGER_USER=you -e TT_MANAGER_PASS=... -e TT_SECRET=... teamtrack
159
+ ```
160
+
161
+ or on anything buildpack-shaped, via the `Procfile`:
162
+
163
+ ```sh
164
+ uvicorn teamtrack.cli:app --host 0.0.0.0 --port $PORT --workers 1 \
165
+ --proxy-headers --forwarded-allow-ips='*'
166
+ ```
167
+
168
+ `--proxy-headers` matters: the same-origin check compares the browser's `Origin`
169
+ against the `Host`/`X-Forwarded-Host` the request arrived on, which is how the app
170
+ is reachable without being told its own hostname.
171
+
172
+ ### One worker
173
+
174
+ Sessions live in memory. Two workers keep two stores, so a session made on one is
175
+ unknown to the other and users bounce between signed in and signed out. In-memory is
176
+ a deliberate trade: the alternative is a file full of live GitHub tokens, which would
177
+ then need its own encryption and key handling to be worth having. A restart signs
178
+ everyone out. Move sessions to a shared store before raising the worker count.
179
+
180
+ ### Reached in laptop mode
181
+
182
+ If the process is running without `TT_HOSTED` and a request arrives at a non-loopback
183
+ hostname, every route answers **503** with the variables to set. That combination is
184
+ almost always a misconfigured deploy, and the failure it would otherwise produce —
185
+ one shared GitHub identity for everybody — is silent and not recoverable after the
186
+ fact.
187
+
188
+ ### Jira is shared, not per-user
189
+
190
+ Hosted, Jira reads one service account from `TT_JIRA_*`, and `status()` reports
191
+ `shared: true`. An Atlassian API token belongs to a person, and there is nowhere to
192
+ put fifty of them that is not a secrets store this app does not have — so anything
193
+ created in Jira is attributed to that one account whoever clicked the button. The
194
+ in-app link form is refused rather than quietly making one person's Jira act for
195
+ everyone. (A container also has no keychain backend, so the write would fail anyway.)
196
+
197
+ ### Still true after all this
198
+
199
+ Hosting fixes identity. It does not fix either of these:
200
+
201
+ * **Rate limits.** The board polls every 30s and costs `2N+1` GitHub calls per poll
202
+ — one listing, N request reads, N status listings. GitHub allows 5,000/hour per
203
+ token, so around 20 open requests saturates one user's budget from a single open
204
+ tab. Each user now has their own budget, which helps, but the fix is a read cache.
205
+ * **Reporting.** Cycle times across hundreds of requests would mean reading every
206
+ event file of every request on each dashboard load. The event log already holds the
207
+ raw material — every status change records who and when — but aggregating it wants
208
+ SQL, not the Contents API.
209
+
210
+ ---
211
+
212
+ ## Teams
213
+
214
+ A team is a folder in the storage repo. Managers create one from the Teams view:
215
+
216
+ 1. **Name** it — becomes `teams/<slug>/`, with its own board, its own components map
217
+ 2. **Set a manager username and password** for that team — optional
218
+ 3. **Invite members** by GitHub username, one per line
219
+
220
+ ### Which folder a write lands in
221
+
222
+ The folder is decided server-side from **who you are**, never from what the browser
223
+ asks for:
224
+
225
+ | Situation | Folder used |
226
+ |---|---|
227
+ | Signed in to a team with its login | that team, always — a request naming a different one is refused |
228
+ | Named a team you are a member of | that team |
229
+ | Named a team you are *not* in | 403 |
230
+ | Named a team that does not exist | 404 — folders no longer spring into existence |
231
+ | Named no team | the shared root, `data/…` |
232
+ | You have `admin`/`maintain` on the repo | any team in it — you own the repo |
233
+
234
+ Membership means: listed in the team's `members`, or the team's creator, or the
235
+ team's own login name.
236
+
237
+ ### The limit of a folder
238
+
239
+ Within the app, the table above holds. It cannot hold **outside** the app.
240
+
241
+ GitHub has no per-folder permission — access is granted per repo. So anyone with
242
+ push access to the repo can write any team's folder by calling the GitHub API
243
+ directly, bypassing this process entirely. A team folder is an organisational
244
+ boundary that this app enforces, not one that GitHub enforces. Point `TT_REPO` at a
245
+ separate repo for a team that needs a line holding no matter how it is approached.
246
+
247
+ ### The team password
248
+
249
+ Stored in `teams/<slug>/team.json` as a PBKDF2-SHA256 digest (200k iterations,
250
+ random per-team salt) — never the password itself, so it cannot be read back out.
251
+ Set it once and share it with the team yourself.
252
+
253
+ Like the global credential, it selects the manager **view**. Every button it
254
+ reveals still re-checks `role_for()` against GitHub before acting.
255
+
256
+ ## Two hats, and who decides what
257
+
258
+ Two independent things gate every write:
259
+
260
+ **Which hat you wear** comes from the sign-in you used — nothing else:
261
+
262
+ | Sign-in | Hat | Can |
263
+ |---|---|---|
264
+ | Manager roster (`manager1`) | manager | read the board, comment, acknowledge / in progress / resolve / reject. **Cannot file.** |
265
+ | Project login (project name + password) | member | file requests, comment, read the board. **Cannot move a ticket.** |
266
+
267
+ **What is possible at all** comes from GitHub, checked on every write: no push
268
+ access, no writes, whatever hat you are wearing. A read-only collaborator cannot
269
+ file, move, or comment.
270
+
271
+ The hat is deliberately *not* derived from GitHub permissions. A manager owns their
272
+ project repo, so GitHub always calls them an admin there — deriving the hat from
273
+ that would leave them unable to sign in as a teammate on their own project at all.
274
+ Sign in as a manager to close tickets; sign in to the project to file one.
275
+
276
+ One consequence, stated plainly: someone with push access who knows a manager
277
+ password can move tickets, where the old rule demanded GitHub admin. That is not a
278
+ real downgrade — push access already lets them write the event file directly through
279
+ the API, bypassing this app. The hat chooses which operations are *offered*; it was
280
+ never a security boundary and does not pretend to be.
281
+
282
+ ## The ticket thread
283
+
284
+ Click a request id on the board to expand it: its fields, then the full activity
285
+ thread replayed from the event files, then a comment box. Managers get
286
+ **Acknowledge / In progress / Mark resolved / Reject** underneath, and whatever is
287
+ in the comment box rides along as the note on that status change.
288
+
289
+ Comments and status changes are both new files, so two people acting on the same
290
+ ticket at the same moment cannot collide. A half-written comment survives the 30s
291
+ board refresh.
292
+
293
+ ### The manager password is break-glass, and it used to be less than that
294
+
295
+ `teamtrack/config.py` holds a password roster seeded with `manager1` / `m@n@ger`;
296
+ `TT_MANAGER_USER`/`TT_MANAGER_PASS` replace it. Because the package is public, the
297
+ default is public.
298
+
299
+ It was once genuinely only a view selector: every manager-only route re-asked GitHub
300
+ whether the caller had push on the repo, so the password got you the manager screen
301
+ and a 403 from every button on it. **That second check went with GitHub.**
302
+ `identity.effective_role()` is the authorisation now, and this password is one of the
303
+ three things that satisfies it — alongside `TT_ADMINS` and the manager roster.
304
+
305
+ So `hosting.check_or_die()` refuses to serve hosted with the default in place, and the
306
+ roster is the route to use: per-person, revocable, and attributable, which a shared
307
+ password is none of.
308
+
309
+ ---
310
+
311
+ ## Linting
312
+
313
+ All local, all deterministic — regexes and set operations, no AI, no network call. Same
314
+ draft in, same warnings out.
315
+
316
+ - **Completeness** — required fields per request type, shown as `filled/required`
317
+ - **Vagueness** — flags "not working", "broken", "asap", "sometimes", … and names what
318
+ to supply instead
319
+ - **Missing evidence** — no error output or repro steps on a bug, no number in
320
+ *affected*, no first-seen time
321
+ - **Extraction** — pulls the exception type and `file:line` out of a pasted traceback,
322
+ linkifies bare URLs, fences code-shaped text, links `REQ-…` ids it finds
323
+ - **Duplicates** — character-trigram Jaccard similarity against every open request,
324
+ warning above 0.55
325
+ - **Severity** — suggests, never overwrites
326
+
327
+ Warnings never block a submit. The author is the one who knows whether a box genuinely
328
+ does not apply.
329
+
330
+ ---
331
+
332
+ ## Blast radius
333
+
334
+ `components.json` is a hand-maintained dependency map, editable per project:
335
+
336
+ ```json
337
+ {
338
+ "airbyte-sync": ["s3-iceberg"],
339
+ "s3-iceberg": ["clickhouse", "dbt-models"],
340
+ "clickhouse": ["client-ui"],
341
+ "dbt-models": ["client-ui"],
342
+ "client-ui": []
343
+ }
344
+ ```
345
+
346
+ Pick a component and the UI breadth-first traverses the map, drawing the reported
347
+ component and everything downstream of it, with the affected count underneath. Pure
348
+ graph traversal — nothing is inferred.
349
+
350
+ ---
351
+
352
+ ## CLI
353
+
354
+ ```sh
355
+ tt # start the UI
356
+ tt --port 8765 --no-browser # fixed port, no browser
357
+ tt whoami # print the signed-in GitHub login
358
+ tt signout # delete the token from the OS keychain
359
+ tt compact <owner> <repo> 2026-07 # archive the repo root
360
+ tt compact <owner> <repo> 2026-07 --team data-platform # archive one team folder
361
+ ```
362
+
363
+ `compact` is the only place Parquet appears. It is a cold archive, never the write
364
+ path — a whole-file rewrite is exactly the shared-mutable-file failure this design
365
+ exists to avoid.
366
+
367
+ ---
368
+
369
+ ## Keyboard
370
+
371
+ | Key | Does |
372
+ |---|---|
373
+ | `cmd/ctrl + enter` | submit the request |
374
+ | `esc` | close the shortcut sheet, or any open inline form |
375
+ | `/` | search the board |
376
+ | `g` then `b` | go to the board |
377
+ | `?` | shortcut sheet |
378
+
379
+ ---
380
+
381
+ ## Development
382
+
383
+ ```sh
384
+ pip install -e .
385
+ export TT_CLIENT_ID=Ov23li...
386
+ tt
387
+ ```
388
+
389
+ The UI is one file — [`teamtrack/static/index.html`](teamtrack/static/index.html),
390
+ vanilla HTML/CSS/JS. No framework, no bundler, no CDN, no webfonts; it works offline.