mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* fix(op-log): lock snapshot save to prevent lost-update window saveCurrentStateAsSnapshot() read NgRx state then lastSeq without holding OPERATION_LOG lock. An op appended between the two reads would get seq <= lastAppliedOpSeq but its effect would be absent from the snapshot. On next hydration the tail replay would start after that seq, silently skipping the op forever. Fix: wrap in lockService.request(LOCK_NAMES.OPERATION_LOG, ...) and read lastSeq BEFORE state snapshot so the worst interleaving degrades to harmless re-replay (idempotent) rather than a missed op. Fixes #8308 * fix(op-log): address review feedback on snapshot lock PR - Amend JSDoc idempotency claim: syncTimeSpent is additive on re-replay - Add inline note about compaction's opposite read order and worse failure mode - Add lock regression tests (#8308): lock acquired, read order, error handling Co-Authored-By: Claude <noreply@anthropic.com> * refactor(sync): remove unused error classes, dialog, and constructor-time logging Phase 1 of #8325: clean up orphaned sync error types and their UI. Removed error classes that are no longer thrown anywhere: - NoEtagAPIError, FileExistsAPIError (unused API errors) - RevMismatchForModelError, SyncInvalidTimeValuesError (superseded by file-based flow) - RevMapModelMismatchErrorOnDownload/Upload, NoRemoteModelFile, NoRemoteMetaFile - LockPresentError, LockFromLocalClientPresentError, MetaNotReadyError, InvalidRevMapError Removed DialogSyncErrorComponent and all references in SyncWrapperService (_forceDownload, _handleIncoherentTimestampsDialog, _handleIncompleteSyncDialog, _openSyncErrorDialog, _extractModelIdFromError). Removed constructor-time logging from JsonParseError, ModelValidationError, DataValidationFailedError — these errors are logged at the catch site; redundant construction-time logs risk leaking user data. Cleaned up dead translation keys (D_INCOMPLETE_SYNC block, DIALOG_RESULT_ERROR, ERROR_DATA_IS_CURRENTLY_WRITTEN) from en.json and t.const.ts. Updated file-based-sync-flowchart.md to reflect the removed error types. --------- Co-authored-by: Claude <noreply@anthropic.com>
458 lines
24 KiB
Markdown
458 lines
24 KiB
Markdown
# Plainspace Integration Plan
|
||
|
||
Integrating **Plainspace** (plainspace.org — repo `Johannesjo/spaces`) into Super
|
||
Productivity (SP) so that:
|
||
|
||
1. A project can be made **shared on Plainspace** directly from the project
|
||
create/edit dialog.
|
||
2. For such shared projects, SP shows — by **task ownership** (see the model in
|
||
§1):
|
||
- **My list** — tasks assigned to me, as regular, editable SP tasks.
|
||
- **A read‑only claim pool** — unclaimed (unassigned) tasks you can **claim**;
|
||
claiming assigns the task to you in Plainspace and imports it as an SP task.
|
||
- Tasks **assigned to others are not represented in SP** at all.
|
||
|
||
> **Conceptual note (revised):** an earlier draft mirrored "assigned to others"
|
||
> into SP as a standing read-only list. We dropped that — SP is a personal focus
|
||
> tool, and a permanent wall of others' non-actionable tasks works against it and
|
||
> creates a stale second copy of the Plainspace board. The model is now: **only
|
||
> _mine_ + _unclaimed_ appear in SP; claiming is the bridge** that turns shared
|
||
> work into your work. See the conversation rationale captured in §7.
|
||
|
||
> Status: planning + prototype. The Plainspace HTTP API contract is not yet
|
||
> pinned down in this document (see [Open questions](#10-open-questions--blocking-decisions));
|
||
> the prototype is built against an **assumed contract** isolated behind a single
|
||
> API service so it can be corrected in one place once the real API is known.
|
||
>
|
||
> **Implemented today (mock-backed):**
|
||
>
|
||
> - §4 — the `PLAINSPACE` issue provider (`providers/plainspace/`): config form,
|
||
> `PlainspaceApiService` (mock mode via `PLAINSPACE_USE_MOCK`),
|
||
> `PlainspaceCommonInterfacesService` implementing `IssueServiceInterface`,
|
||
> registered in `issue.model.ts` / `issue.const.ts` / `issue.service.ts` +
|
||
> icon. **Only tasks assigned to me** import via the issue→backlog pipeline.
|
||
> - §5 — account / identity: `PlainspaceAccountService` (signals: `account`,
|
||
> `isLoggedIn`, `currentUserId`; localStorage-persisted, never synced) with a
|
||
> mock `login`/`logout`. "Mine" comes from the signed-in identity, and the
|
||
> share toggle prompts sign-in if needed.
|
||
> - §6 — the "Share on Plainspace" toggle in the create-project dialog, which
|
||
> (after sign-in) provisions a (mock) space and a bound provider via
|
||
> `PlainspaceShareService`.
|
||
> - §7 — the read-only **claim pool**: `PlainspaceClaimPoolService` feeds
|
||
> unclaimed tasks (mock) through `project-task-page` → `work-view` into a
|
||
> collapsed-by-default panel (`PlainspaceClaimPoolComponent`). A **Claim**
|
||
> action assigns the task to me and imports it as an SP task. Shows only for
|
||
> shared projects.
|
||
>
|
||
> **Still design-only:** §8 write-back, and the real HTTP API + real auth (all
|
||
> `PlainspaceApiService` calls, the login, and claim are mocked — see §10). The
|
||
> claim pool does not yet auto-poll (loads on project open / provider change /
|
||
> after a claim).
|
||
|
||
---
|
||
|
||
## 1. Guiding decisions (agreed)
|
||
|
||
| Decision | Choice |
|
||
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||
| **Source of truth** for shared data | **Plainspace backend.** SP reads/writes shared tasks via Plainspace's API as a separate channel from SP's own op‑log sync. |
|
||
| **Integration shape** | Model Plainspace as a **regular issue provider** (like Jira/Redmine) "for the most part". |
|
||
| **Identity** | **Plainspace account login** (token-based). The authenticated account defines "me" — which tasks are mine vs unclaimed. |
|
||
| **v1 scope** | Plan the full feature; build a working **prototype** (UI + provider scaffold against an assumed/mock API). |
|
||
|
||
### Why "issue provider" is the right host
|
||
|
||
SP already has a mature, well-factored issue-provider system (Jira, GitLab,
|
||
CalDAV, OpenProject, Trello, Redmine, Azure DevOps, Nextcloud Deck). It gives us,
|
||
for free:
|
||
|
||
- Per-provider config + Formly config form, stored in the issue-provider NgRx
|
||
store and bindable to a specific project via `defaultProjectId`.
|
||
- Search in the add-task bar, "add issue as task", attachments mapping.
|
||
- **Auto-import to backlog** (`getNewIssuesToAddToBacklog`) and **polling for
|
||
fresh data** (`getFreshDataForIssueTask`) with a configurable `pollInterval`.
|
||
- A clean single interface to implement: `IssueServiceInterface`.
|
||
|
||
This means "Plainspace issues assigned to me" flow through the **existing**
|
||
issue→task pipeline with almost no new core code. The only genuinely new surface
|
||
is the read-only **claim pool**, because that data is _not_ imported as SP tasks
|
||
until claimed.
|
||
|
||
### The one important nuance
|
||
|
||
The standard issue-provider flow turns issues **into** SP tasks. We only do that
|
||
for tasks **assigned to me**. Unclaimed tasks are _shown_ (a read-only pool) but
|
||
**not** auto-imported — claiming is a deliberate act that assigns the task to me
|
||
and then imports it. Tasks assigned to others are not represented in SP at all.
|
||
(Auto-importing _unclaimed_ work as if it were yours has the same problem as the
|
||
old others-list, just subtler: two members connecting the same space would both
|
||
"own" the same unclaimed task locally.) See §7 for the full ownership model.
|
||
|
||
---
|
||
|
||
## 2. Architecture overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Plainspace API │
|
||
│ spaces (projects) · tasks · members · auth │
|
||
└───────────────┬───────────────┬─────────────┘
|
||
│ │
|
||
(issue-provider channel) (shared-project channel)
|
||
│ │
|
||
┌────────────────────────────────▼──┐ ┌────────▼───────────────────────────┐
|
||
│ PlainspaceApiService (HTTP) │ │ PlainspaceAccountService (auth/me) │
|
||
│ PlainspaceCommonInterfacesService │ │ PlainspaceClaimPoolService │
|
||
│ implements IssueServiceInterface │ │ (unclaimed tasks + claim action) │
|
||
└───────────────┬────────────────────┘ └───────────────┬────────────────────┘
|
||
│ │
|
||
┌───────────────▼────────────────┐ ┌────────────────▼───────────────────┐
|
||
│ Existing issue→task pipeline │ │ Read-only claim-pool panel │
|
||
│ → real SP tasks (assigned to me)│ claim │ in work-view (unclaimed tasks) │
|
||
└─────────────────────────────────┘◄───────┴─────────────────────────────────────┘
|
||
│ │
|
||
└──────────────► Project work view ◄───────┘
|
||
(My list) (Claim pool)
|
||
```
|
||
|
||
- **Issue-provider channel** = the Jira-like path. Registers a `PLAINSPACE`
|
||
provider, bound per project via `defaultProjectId` (= the SP project the space
|
||
maps to). Imports **assigned-to-me** issues, polls them for freshness.
|
||
- **Shared-project channel** = the new bits: account login, the unclaimed claim
|
||
pool (+ claim → import), and creating a space when a project is shared.
|
||
|
||
SP's own op-log/vector-clock sync is **untouched**: shared data does not flow
|
||
through it. (Doing so would mean teaching the single-user op-log to carry
|
||
multi-user ops — explicitly rejected as too risky.)
|
||
|
||
---
|
||
|
||
## 3. Data model changes
|
||
|
||
### 3.1 New issue-provider config (`PlainspaceCfg`)
|
||
|
||
New folder `src/app/features/issue/providers/plainspace/`. Config interface (mirrors
|
||
`RedmineCfg`):
|
||
|
||
```ts
|
||
export interface PlainspaceCfg extends BaseIssueProviderCfg {
|
||
host: string | null; // plainspace.org or self-hosted base URL
|
||
spaceId: string | null; // the Plainspace "space" this provider is bound to
|
||
token?: string | null; // PAT (pat_…) authorizing this provider's API calls
|
||
}
|
||
```
|
||
|
||
> **Where the token lives (as built).** The PAT is stored on `PlainspaceCfg.token`
|
||
> and authorizes every `PlainspaceApiService` call — exactly like Jira's
|
||
> `password` or CalDAV's `password`, and like them it is part of synced
|
||
> issue-provider state and is included in plaintext backups/exports. This is a
|
||
> deliberate parity choice (a provider works on a fresh device after sync without
|
||
> re-pasting), and is the accepted secret-handling posture for issue providers.
|
||
> The account store (§3.3, local-only `localStorage`) holds a token **too**, but
|
||
> only to bootstrap the "Share on Plainspace" flow, which needs a token _before_
|
||
> any provider exists; the provider runtime reads only `cfg.token`. An earlier
|
||
> draft said the token was not stored in the cfg — that was never the case in the
|
||
> shipped code.
|
||
|
||
### 3.2 Plainspace issue/task shapes (assumed — single source to fix later)
|
||
|
||
```ts
|
||
// src/app/features/issue/providers/plainspace/plainspace-issue.model.ts
|
||
export interface PlainspaceMember {
|
||
id: string;
|
||
name: string;
|
||
avatarUrl?: string;
|
||
}
|
||
|
||
export interface PlainspaceIssue {
|
||
id: string;
|
||
title: string;
|
||
isDone: boolean;
|
||
assigneeId: string | null; // null = unassigned
|
||
assignee?: PlainspaceMember | null;
|
||
updatedAt: string; // ISO
|
||
url?: string;
|
||
// ...extend once the real API is known
|
||
}
|
||
```
|
||
|
||
### 3.3 Account / identity (new, small store)
|
||
|
||
```ts
|
||
// src/app/features/plainspace/plainspace-account.model.ts
|
||
export interface PlainspaceAccount {
|
||
host: string; // which plainspace instance
|
||
userId: string; // "me"
|
||
displayName: string;
|
||
token: string; // bearer token (stored like other provider creds)
|
||
}
|
||
```
|
||
|
||
Stored per SP profile alongside other credentials (same mechanism existing
|
||
providers use for secrets). One account → many spaces.
|
||
|
||
### 3.4 No change to the SP `Task` model in v1
|
||
|
||
- "Mine/unassigned" tasks are normal SP tasks; their Plainspace origin is already
|
||
captured by the existing `issueId` / `issueProviderId` / `issueType` fields.
|
||
- "Assigned to others" tasks are **not** SP tasks, so they need no `Task` field.
|
||
An `assignee` field on SP tasks is **explicitly deferred** (would touch the
|
||
hot-path task component and sync) — see [Future work](#11-future-work).
|
||
|
||
### 3.5 Project ↔ space link
|
||
|
||
The link is expressed entirely through the issue-provider instance:
|
||
`IssueProviderPlainspace.defaultProjectId` = SP project id, and
|
||
`PlainspaceCfg.spaceId` = remote space id. No new field on `Project` is strictly
|
||
required. (Optional convenience flag `Project.isSharedOnPlainspace` could be added
|
||
later for menu/badge rendering, but is not needed for correctness.)
|
||
|
||
---
|
||
|
||
## 4. Phase 1 — Plainspace issue provider scaffold
|
||
|
||
Goal: `PLAINSPACE` exists as a first-class issue provider; my/unassigned issues
|
||
import as tasks and poll. Pattern reference: **Redmine** (simplest built-in).
|
||
|
||
### 4.1 Central registration (4 edits)
|
||
|
||
- `src/app/features/issue/issue.model.ts`
|
||
- add `'PLAINSPACE'` to `BuiltInIssueProviderKey` + `BUILT_IN_KEYS`
|
||
- add `PlainspaceCfg` to `IssueIntegrationCfg` union and
|
||
`IssueIntegrationCfgs` map
|
||
- add issue type to `IssueData` / `IssueDataReduced` (+ `IssueDataReducedMap`)
|
||
- add `IssueProviderPlainspace extends IssueProviderBase, PlainspaceCfg`
|
||
(`issueProviderKey: 'PLAINSPACE'`) and add it to the `IssueProvider` union and
|
||
`IssueProviderTypeMap`.
|
||
- `src/app/features/issue/issue.const.ts`
|
||
- `PLAINSPACE_TYPE`, add to `ISSUE_PROVIDER_TYPES`,
|
||
`ISSUE_PROVIDER_ICON_MAP`, `ISSUE_PROVIDER_HUMANIZED`,
|
||
`DEFAULT_ISSUE_PROVIDER_CFGS`, `ISSUE_PROVIDER_FORM_CFGS_MAP`, `ISSUE_STR_MAP`.
|
||
- `src/app/features/issue/issue.service.ts`
|
||
- import + inject `PlainspaceCommonInterfacesService`, add to
|
||
`ISSUE_SERVICE_MAP`.
|
||
- Provider icon: add `src/assets/icons/plainspace.svg` **and** register it in
|
||
`GlobalThemeService` (`_initIcons()`, the `addSvgIcon(...)` block) — the
|
||
`ISSUE_PROVIDER_ICON_MAP` value only names the icon, it does not register it.
|
||
Note `ISSUE_PROVIDER_HUMANIZED` is a plain string ('Plainspace'), not a `T`
|
||
key, so no translation entry is needed for the provider name itself.
|
||
|
||
> Not strictly 4 files: adding `'PLAINSPACE'` to `BuiltInIssueProviderKey` also
|
||
> widens `IssueProviderKey`, so the existing `Task.issueType` field gains
|
||
> `'PLAINSPACE'` as a valid value. No new `Task` field, but it is a (safe,
|
||
> additive) type-surface change to be aware of.
|
||
|
||
### 4.2 New provider files (`providers/plainspace/`)
|
||
|
||
| File | Responsibility |
|
||
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||
| `plainspace.model.ts` | `PlainspaceCfg` |
|
||
| `plainspace-issue.model.ts` | `PlainspaceIssue`, `PlainspaceMember` |
|
||
| `plainspace.const.ts` | `DEFAULT_PLAINSPACE_CFG`, `PLAINSPACE_POLL_INTERVAL` |
|
||
| `plainspace-cfg-form.const.ts` | Formly config form + `..._CONFIG_FORM_SECTION` (host, advanced common fields) |
|
||
| `plainspace-api.service.ts` | All HTTP: `searchIssues$`, `getById$`, `getTasksForSpace$`, `getMembers$`, `createSpace$`, plus mock mode |
|
||
| `plainspace-common-interfaces.service.ts` | implements `IssueServiceInterface` (extends `BaseIssueProviderService`) |
|
||
| `plainspace-issue-map.util.ts` | `PlainspaceIssue → SearchResultItem` and `→ getAddTaskData` |
|
||
|
||
### 4.3 `IssueServiceInterface` implementation notes
|
||
|
||
- `isEnabled(cfg)` → `cfg.isEnabled && !!cfg.host && !!cfg.spaceId` and an account
|
||
token present.
|
||
- `getAddTaskData(issue)` → `{ title, isDone, issuePoints? }`. **Filter at the
|
||
source**: only my/unassigned issues are ever offered to this path (see 4.4).
|
||
- `getNewIssuesToAddToBacklog(providerId, existingIds)` → fetch space tasks
|
||
where `assigneeId === me || assigneeId == null`, minus `existingIds`.
|
||
- `getFreshDataForIssueTask(task)` → re-fetch by id, return `isDone`/title
|
||
changes (never overwrite user scheduling).
|
||
- `pollInterval` → `PLAINSPACE_POLL_INTERVAL` (e.g. 5 min). Reuses existing
|
||
`poll-issue-updates.effects.ts` and `poll-to-backlog.effects.ts`.
|
||
|
||
### 4.4 The mine/unassigned filter
|
||
|
||
Centralize in `plainspace-api.service` (`getMyAndUnassignedTasks$`) so both the
|
||
backlog import and the search path only ever see items that are valid to import.
|
||
"Assigned to others" is fetched by a sibling method and never reaches the issue
|
||
pipeline.
|
||
|
||
---
|
||
|
||
## 5. Phase 2 — Account login / identity
|
||
|
||
Goal: establish "me" so the assigned/unassigned split is meaningful.
|
||
|
||
- `src/app/features/plainspace/plainspace-account.service.ts` — login (token
|
||
exchange), store/clear account, expose `me$` (signal) and `currentUserId`.
|
||
- Login UI: a button in the Plainspace provider config form (`testConnection`
|
||
doubles as "verify login"), and/or a small dialog. Token persisted via the same
|
||
secret-storage path other providers use.
|
||
- `isEnabled` and the shared-tasks fetch both depend on a valid account; surface a
|
||
clear "not logged in" state.
|
||
|
||
Auth mechanism (token vs OAuth redirect) depends on what Plainspace exposes — see
|
||
[Open questions](#10-open-questions--blocking-decisions). If OAuth is required,
|
||
reuse the existing `src/app/plugins/oauth/` helpers.
|
||
|
||
---
|
||
|
||
## 6. Phase 3 — "Share on Plainspace" in the project create/edit dialog
|
||
|
||
Goal: a toggle in `dialog-create-project` that provisions a Plainspace space and
|
||
wires up the provider binding.
|
||
|
||
- **Form**: add an `isShareOnPlainspace` checkbox to
|
||
`CREATE_PROJECT_BASIC_CONFIG_FORM_CONFIG`
|
||
(`src/app/features/project/project-form-cfg.const.ts`). Gate it behind "account
|
||
logged in" (show a login affordance if not).
|
||
- **On submit** (in `dialog-create-project.component.ts`), after the project is
|
||
created via `projectService.add()`:
|
||
1. If `isShareOnPlainspace` and logged in → `PlainspaceApiService.createSpace$(
|
||
{ title })` → returns `spaceId`.
|
||
2. Create a `PLAINSPACE` issue-provider instance with
|
||
`{ host, spaceId, isEnabled: true, defaultProjectId: <newProjectId>,
|
||
isAutoAddToBacklog: true }` via the issue-provider store.
|
||
- **Edit mode**: same toggle reflects whether a bound Plainspace provider exists;
|
||
turning it on later provisions the space + provider; turning it off should
|
||
prompt (unlink vs delete remote) — keep v1 to **unlink only** (disable provider,
|
||
leave remote space intact) to avoid destructive surprises.
|
||
- **i18n**: new strings via `T`/`en.json` only.
|
||
|
||
---
|
||
|
||
## 7. Ownership model & the claim pool (revised)
|
||
|
||
**The conceptual question that reshaped this feature:** _which tasks from a
|
||
shared space should appear in a personal focus app at all?_ Answer — by
|
||
ownership state:
|
||
|
||
| Plainspace state | In SP? | Treatment |
|
||
| -------------------- | ------------------- | ------------------------------------------------------------- |
|
||
| Assigned to me | Yes — the point | First-class SP tasks: schedule, time-track, complete. |
|
||
| Unclaimed | As a pool, not list | Read-only "claim pool"; **Claim** → assign-to-self → SP task. |
|
||
| Assigned to others | No | Not represented in SP (Plainspace board is the team view). |
|
||
| Done (others/unass.) | No | Irrelevant to the individual. |
|
||
|
||
**Claiming is the bridge** between the collective space and the personal app:
|
||
the only way unclaimed work becomes yours. This avoids mirroring a stale,
|
||
non-actionable copy of other people's tasks into a focus tool. (We also skip
|
||
per-task assignee badges: with only _mine_ + _unclaimed_ shown, ownership is
|
||
implicit, and SP already shows a provider icon on issue-linked tasks.)
|
||
|
||
### 7.1 Read-only claim-pool component
|
||
|
||
`src/app/features/plainspace/claim-pool/claim-pool.component.ts`
|
||
|
||
- Input: `tasks: PlainspaceSharedTask[]` (unclaimed) + `projectId`.
|
||
- Flat read-only rows: title, "open in Plainspace" link, **Claim** button. No
|
||
drag/drop, scheduling, time tracking, or task-store interaction.
|
||
|
||
> Deliberately a **lightweight standalone component**, not the hot-path
|
||
> `task.component` — these are foreign tasks until claimed.
|
||
|
||
### 7.2 Data flow
|
||
|
||
- `PlainspaceClaimPoolService.unclaimedTasksForProject$(projectId)` finds the
|
||
bound enabled `PLAINSPACE` provider and returns its unclaimed tasks
|
||
(`assigneeId === null && !isDone`), refreshing after a claim.
|
||
- `claim(projectId, taskId)` → `PlainspaceApiService.claimTask$` (assign-to-me)
|
||
→ `IssueService.addTaskFromIssue(... isAddToBacklog)` → pool refreshes.
|
||
- `project-task-page` derives `unclaimedTasks` and passes it (+ `projectId`)
|
||
into `work-view`.
|
||
|
||
### 7.3 Layout
|
||
|
||
A `collapsible` section (mirrors overdue/done panels), **collapsed by default**
|
||
(it's a pool you reach for, not your active work); state persisted in
|
||
`localStorage` (`LS.PLAINSPACE_CLAIM_POOL_HIDDEN`).
|
||
|
||
---
|
||
|
||
## 8. Phase 5 — Polling, refresh & write-back
|
||
|
||
- **Reads**: reuse issue polling for my tasks; add a light timer in
|
||
`PlainspaceClaimPoolService` to refresh the claim pool (same interval), only
|
||
while the shared project is open. Today it refreshes on project open / provider
|
||
change / after a claim — no timer yet.
|
||
- **Writes (mine)**: completing/editing a _my_ imported task should optionally
|
||
push back to Plainspace via `updateIssueFromTask` (the optional interface hook).
|
||
Start **read-mostly**: import + status sync for done-state only; expand later.
|
||
- **Claim**: the one write already implemented — assign an unclaimed task to me
|
||
(`claimTask$`), then import it. Real mode would POST the assignment.
|
||
- **Offline**: all Plainspace calls must fail soft (empty lists, cached last
|
||
values) and never block the SP UI; SP remains fully usable offline.
|
||
|
||
---
|
||
|
||
## 9. Prototype scope (this iteration)
|
||
|
||
A runnable prototype that demonstrates the UX end-to-end against a **mock**
|
||
Plainspace backend (toggled by a flag in `PlainspaceApiService`), so it works with
|
||
no live server and is trivially swapped for the real API:
|
||
|
||
1. `PLAINSPACE` provider registered + config form (Phase 1 skeleton).
|
||
2. `PlainspaceApiService` with a **mock mode** returning canned spaces, members,
|
||
and tasks (mix of mine / unassigned / others).
|
||
3. "Share on Plainspace" toggle in the create dialog that, in mock mode, fakes
|
||
space creation and provisions the provider binding (Phase 3).
|
||
4. The **"Assigned to others"** read-only panel wired into the work view for
|
||
shared projects (Phase 4) — the visually novel part.
|
||
5. My/unassigned issues importing into the normal list via the issue pipeline.
|
||
|
||
Out of prototype scope: real auth handshake, write-back, attachments, subtasks,
|
||
production error/empty states polish.
|
||
|
||
---
|
||
|
||
## 10. Open questions / blocking decisions
|
||
|
||
These need answers (ideally the Plainspace API docs / the `Johannesjo/spaces`
|
||
repo, which I could not access from this environment) to move the prototype onto
|
||
the real backend:
|
||
|
||
1. **Auth**: token/API-key, or OAuth redirect flow? Endpoint(s)? How is "me"
|
||
(current user id) returned?
|
||
2. **Spaces API**: create space (`POST`?), list spaces, get members of a space.
|
||
3. **Tasks API**: list/get tasks for a space; fields available (esp. `assigneeId`,
|
||
done state, ordering); search; pagination.
|
||
4. **Write-back**: can SP create/update tasks and assignments? Required for the
|
||
"share" flow to push SP tasks up, vs. pull-only.
|
||
5. **Hosting**: is it always plainspace.org, or self-hostable (host field needed)?
|
||
6. **CORS/Electron**: does the API allow browser-origin calls, or must requests go
|
||
through the Electron main process (like some providers do)?
|
||
|
||
## 11. Future work
|
||
|
||
- Optional `Project.isSharedOnPlainspace` flag for menu badges.
|
||
- A real `assignee` concept on SP tasks (hot-path + sync implications — separate
|
||
design).
|
||
- Reassign-from-SP, presence/avatars, comments, two-way task creation.
|
||
|
||
## 12. Risks
|
||
|
||
- **Cross-client forward-compat (rollout-gating).** Adding `'PLAINSPACE'` to the
|
||
built-in `IssueProviderKey` union widens a **synced, typia-validated** type
|
||
(`issueProvider` state, and `task.issueType`). A client built before this change
|
||
has an AOT-baked validator that does not know the `'PLAINSPACE'` literal, so
|
||
when a newer client creates a Plainspace provider and syncs it, the older client
|
||
will reject the incoming model as corrupt (the documented "typia rejects unknown
|
||
union members" failure → false data-corruption dialog / rejected sync). This is
|
||
inherent to adding any new built-in provider key, and the mitigation is
|
||
**release sequencing**: the validator-aware build must reach the fleet **before**
|
||
any client can emit a `PLAINSPACE` provider. Do not back-port the ability to
|
||
create a Plainspace provider to a client that can't validate the key. (The
|
||
alternative — modelling Plainspace as a `plugin:`-style opaque key, which the
|
||
validator already accepts — is forward-compatible but is a larger change and is
|
||
the wrong shape for a built-in provider; revisit only if simultaneous rollout
|
||
can't be guaranteed.)
|
||
- **Sync correctness**: keep shared data **out** of the op-log; never route
|
||
Plainspace fetches through NgRx persisted actions. Imported "my" tasks follow
|
||
the existing, already-correct issue-task path.
|
||
- **Hot path**: the claim pool is a new lightweight component, not
|
||
`task.component`; verify against large lists.
|
||
- **API assumptions**: all isolated in `PlainspaceApiService` +
|
||
`plainspace-issue.model.ts` so the real contract changes one layer.
|
||
- **Privacy**: the PAT is stored in synced provider cfg like other provider
|
||
secrets (see §3.1); no analytics; log only ids (`Log.log({ id })`).
|
||
|
||
```
|
||
|
||
```
|