@skillcdn/console 0.3.0 → 0.3.1

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 (129) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +185 -28
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +5 -0
  11. package/dist/cli/credentials.d.ts.map +1 -1
  12. package/dist/cli/credentials.js +2 -0
  13. package/dist/cli/credentials.js.map +1 -1
  14. package/dist/cli/directory.d.ts +7 -0
  15. package/dist/cli/directory.d.ts.map +1 -1
  16. package/dist/cli/directory.js +7 -0
  17. package/dist/cli/directory.js.map +1 -1
  18. package/dist/cli/format.d.ts +10 -1
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +67 -13
  21. package/dist/cli/format.js.map +1 -1
  22. package/dist/cli/help.d.ts.map +1 -1
  23. package/dist/cli/help.js +30 -13
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/client.d.ts +26 -1
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +14 -1
  28. package/dist/client.js.map +1 -1
  29. package/dist/components/calendar.d.ts +5 -1
  30. package/dist/components/calendar.d.ts.map +1 -1
  31. package/dist/components/calendar.js +3 -3
  32. package/dist/components/calendar.js.map +1 -1
  33. package/dist/components/event-feed.d.ts.map +1 -1
  34. package/dist/components/event-feed.js +2 -0
  35. package/dist/components/event-feed.js.map +1 -1
  36. package/dist/components/pack-connect.d.ts +23 -0
  37. package/dist/components/pack-connect.d.ts.map +1 -0
  38. package/dist/components/pack-connect.js +86 -0
  39. package/dist/components/pack-connect.js.map +1 -0
  40. package/dist/components/pack-token.d.ts +27 -0
  41. package/dist/components/pack-token.d.ts.map +1 -0
  42. package/dist/components/pack-token.js +11 -0
  43. package/dist/components/pack-token.js.map +1 -0
  44. package/dist/components/projects.d.ts +13 -0
  45. package/dist/components/projects.d.ts.map +1 -1
  46. package/dist/components/projects.js +37 -5
  47. package/dist/components/projects.js.map +1 -1
  48. package/dist/components/work.d.ts +3 -1
  49. package/dist/components/work.d.ts.map +1 -1
  50. package/dist/components/work.js +1 -1
  51. package/dist/components/work.js.map +1 -1
  52. package/dist/components/workspace.d.ts +46 -0
  53. package/dist/components/workspace.d.ts.map +1 -0
  54. package/dist/components/workspace.js +124 -0
  55. package/dist/components/workspace.js.map +1 -0
  56. package/dist/console.css +156 -0
  57. package/dist/console.d.ts.map +1 -1
  58. package/dist/console.js +60 -21
  59. package/dist/console.js.map +1 -1
  60. package/dist/data.d.ts +16 -1
  61. package/dist/data.d.ts.map +1 -1
  62. package/dist/data.js +37 -0
  63. package/dist/data.js.map +1 -1
  64. package/dist/dates.d.ts +12 -2
  65. package/dist/dates.d.ts.map +1 -1
  66. package/dist/dates.js +20 -2
  67. package/dist/dates.js.map +1 -1
  68. package/dist/i18n/en.d.ts +53 -0
  69. package/dist/i18n/en.d.ts.map +1 -1
  70. package/dist/i18n/en.js +65 -8
  71. package/dist/i18n/en.js.map +1 -1
  72. package/dist/i18n/ko.d.ts.map +1 -1
  73. package/dist/i18n/ko.js +60 -7
  74. package/dist/i18n/ko.js.map +1 -1
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +3 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/router.d.ts +4 -4
  80. package/dist/router.d.ts.map +1 -1
  81. package/dist/router.js +11 -2
  82. package/dist/router.js.map +1 -1
  83. package/dist/routes.d.ts +4 -0
  84. package/dist/routes.d.ts.map +1 -1
  85. package/dist/routes.js +4 -0
  86. package/dist/routes.js.map +1 -1
  87. package/dist/schemas.d.ts +182 -2
  88. package/dist/schemas.d.ts.map +1 -1
  89. package/dist/schemas.js +110 -13
  90. package/dist/schemas.js.map +1 -1
  91. package/dist/vocabulary.d.ts +19 -1
  92. package/dist/vocabulary.d.ts.map +1 -1
  93. package/dist/vocabulary.js +16 -0
  94. package/dist/vocabulary.js.map +1 -1
  95. package/docs/specs/cli.md +16 -11
  96. package/docs/specs/rest.md +31 -8
  97. package/package.json +2 -2
  98. package/skills/building-a-console/SKILL.md +5 -5
  99. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  100. package/skills/deploying-a-console/SKILL.md +8 -7
  101. package/skills/working-the-board/SKILL.md +2 -2
  102. package/skills/working-the-board/references/setting-up.md +1 -1
  103. package/skills/working-the-board/references/terms.md +2 -0
  104. package/skills/working-the-board/references/the-sentence.md +1 -1
  105. package/src/api.ts +16 -0
  106. package/src/cli/cli.ts +253 -33
  107. package/src/cli/credentials.ts +7 -0
  108. package/src/cli/directory.ts +9 -0
  109. package/src/cli/format.ts +72 -13
  110. package/src/cli/help.ts +36 -13
  111. package/src/client.ts +84 -0
  112. package/src/components/calendar.tsx +8 -2
  113. package/src/components/event-feed.tsx +2 -0
  114. package/src/components/pack-connect.tsx +179 -0
  115. package/src/components/pack-token.tsx +74 -0
  116. package/src/components/projects.tsx +108 -9
  117. package/src/components/work.tsx +4 -1
  118. package/src/components/workspace.tsx +298 -0
  119. package/src/console.tsx +173 -29
  120. package/src/data.ts +57 -0
  121. package/src/dates.ts +23 -2
  122. package/src/i18n/en.ts +76 -8
  123. package/src/i18n/ko.ts +67 -7
  124. package/src/index.ts +16 -0
  125. package/src/router.ts +17 -5
  126. package/src/routes.ts +4 -0
  127. package/src/schemas.ts +129 -12
  128. package/src/styles/console.css +156 -0
  129. package/src/vocabulary.ts +25 -0
package/src/cli/help.ts CHANGED
@@ -136,9 +136,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
136
136
  },
137
137
  use: {
138
138
  usage: "use <key> [--url <origin>] [--account <login>]",
139
- synopsis: "Binds this directory to a project: writes .skillcdn-console.json here.",
139
+ synopsis:
140
+ "Binds this directory to a project: writes .skillcdn-console.json here, and prints the sentence for the checkout's agreement.",
140
141
  about:
141
- "Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names.",
142
+ "Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names. Then prints the sentence for the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), to be copied as it is, since nothing sends an agent to the console until the checkout says so.",
142
143
  refusals: ["project.not_found: no project has the key, or it is not yours to see"],
143
144
  },
144
145
  work: {
@@ -203,9 +204,26 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
203
204
  },
204
205
  people: {
205
206
  usage: "people",
206
- synopsis: "Everyone of the workspace, by login, with what each is in it.",
207
+ synopsis: "The people you see, by login, with what each is in the workspace.",
207
208
  about:
208
- "Everyone of the workspace, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Then the logins an administrator let in that nobody has signed in with yet. The logins are what --assignee, member and person take.",
209
+ "The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --assignee, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
210
+ },
211
+ workspace: {
212
+ usage:
213
+ "workspace | workspace edit [--people-visibility <everyone|shared-projects>] [--week-start <auto|monday|sunday>] [--time-zone <zone|none>] | workspace pack-token --connect | workspace pack-token --stdin | workspace pack-token --remove",
214
+ overview:
215
+ "workspace | workspace edit [flags] | workspace pack-token --connect|--stdin|--remove",
216
+ synopsis:
217
+ "The workspace's settings; or some changed, or its SkillCDN token set, for an administrator.",
218
+ about:
219
+ "What the workspace is called; who sees whom on the People page and in the pickers (everyone; or shared-projects: a member sees only the people they share a project with, an administrator everyone; a project open to the workspace has everyone in it, so a member of one sees everyone); the day the calendars begin a week on (auto: the system default, as each device has it); the workspace's time zone, an IANA name such as Asia/Seoul, in which this command prints moments as ISO 8601 with its offset, as the login kept it at login or whoami, or none, which leaves them as the API gives them, in UTC; where the organization's skills come from; and the most days a token may last. edit changes any of the first three, for an administrator; --json leaves moments as the API gives them. pack-token is the SkillCDN token the organization's pack is read with, by every project that names no address of its own and holds no token of its own (ADR-0036), for an administrator: with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody; with --stdin, a token made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away.",
220
+ refusals: [
221
+ "auth.forbidden: only an administrator changes the workspace's settings or its token",
222
+ "request.invalid: the time zone is not a name the console knows",
223
+ "workspace.pack_token_refused: SkillCDN does not serve the organization's pack to the token, or the console names no address for the organization's skills",
224
+ "workspace.pack_connect_refused: the console names no address for the organization's skills, the pack is on the console's machine, or SkillCDN refused to begin",
225
+ "workspace.pack_token_unsupported, workspace.pack_connect_unsupported: the console keeps no secret to seal a token with (AUTH_SECRET), or reaches no deployment",
226
+ ],
209
227
  },
210
228
  person: {
211
229
  usage:
@@ -224,12 +242,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
224
242
  },
225
243
  project: {
226
244
  usage:
227
- 'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --stdin | project pack-token <key> --remove',
245
+ 'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --connect | project pack-token <key> --stdin | project pack-token <key> --remove',
228
246
  overview:
229
- "project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --stdin|--remove",
230
- synopsis: "One project in full; or a project made, changed, given its mark or its pack token.",
247
+ "project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --connect|--stdin|--remove",
248
+ synopsis:
249
+ "One project in full; or a project made, changed, given its mark or its SkillCDN token.",
231
250
  about:
232
- "One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its pack token (ADR-0028), the token a person made at SkillCDN for a private pack, read from standard input and never an argument, which the console keeps sealed to read the pack with, or taken away. The organization's skills are the project's unless --skills names an address of its own.",
251
+ "One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its SkillCDN token (pack-token, ADR-0028), which the console keeps sealed to read a private pack with, and which a project that reads the organization's pack may leave to the workspace's (console workspace pack-token): with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody (ADR-0036); with --stdin, a token a person made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away. The organization's skills are the project's unless --skills names an address of its own.",
233
252
  limits: [
234
253
  `key: ${KEY}, up to ${MAX_PROJECT_KEY_LENGTH} characters; it does not change once made`,
235
254
  `name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
@@ -243,13 +262,16 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
243
262
  "project.invalid_address: the skills address is not one as SkillCDN spells it",
244
263
  "project.invalid_icon: the file is not an image of those kinds, by its bytes",
245
264
  "request.too_large: the image is over the limit",
265
+ "project.pack_connect_unsupported: the console reaches no deployment, or keeps no secret to seal a token with",
266
+ "project.pack_connect_refused: the project names no pack, the pack is on the console's machine, or SkillCDN refused to begin",
267
+ "skills.unavailable: SkillCDN could not be reached; try again later",
246
268
  ],
247
269
  },
248
270
  members: {
249
271
  usage: "members",
250
- synopsis: "Those listed in the project, with what each is in it.",
272
+ synopsis: "The members of the project, with what each is in it.",
251
273
  about:
252
- "Those listed in the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. A project open to the workspace has everyone of it as a member besides, and an administrator owns every project.",
274
+ "The members of the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. In a private project, those listed; in one open to the workspace, everyone of it, an owner where listed as one or an administrator, a member otherwise. An administrator owns every project.",
253
275
  },
254
276
  member: {
255
277
  usage:
@@ -257,11 +279,12 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
257
279
  synopsis:
258
280
  "Lists a person in the project, changes what they are in it, or takes them off; for an owner.",
259
281
  about:
260
- "Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
282
+ "Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. In a project open to the workspace everyone is a member already: only an owner is listed (member add --role owner, or member role <login> owner), and an owner made a member again stays the member everyone is. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
261
283
  refusals: [
262
284
  "auth.forbidden: only an owner of the project lists its members",
263
285
  "member.invalid_person: the person is not of the workspace",
264
286
  "member.exists: the person is listed already; member role changes what they are",
287
+ "member.open_project: the project is open to the workspace, so everyone is a member already; list an owner with --role owner",
265
288
  "member.not_found: the person is not listed in the project",
266
289
  ],
267
290
  },
@@ -328,7 +351,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
328
351
  usage: "take <number|id> [--agent <name>]",
329
352
  synopsis: "Takes a task: a run begins, and the task is in progress and the person's.",
330
353
  about:
331
- "Takes a task: a run begins, and the task is in progress and the person's; the rules of the board are printed with the task. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
354
+ "Takes a task: a run begins, and the task is in progress and the person's; the run's id and the task's id are printed on a line of their own, then the task, then the rules of the board. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
332
355
  limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
333
356
  refusals: [
334
357
  "task.not_found: no task has that number or id",
@@ -604,7 +627,7 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
604
627
  {
605
628
  title:
606
629
  "Configuring, as the person: what an administrator or an owner does on the pages, their agent does here, named on it",
607
- commands: ["people", "person", "project", "members", "member", "category"],
630
+ commands: ["workspace", "people", "person", "project", "members", "member", "category"],
608
631
  },
609
632
  ];
610
633
 
package/src/client.ts CHANGED
@@ -44,6 +44,8 @@ import {
44
44
  type RestMemberPatch,
45
45
  type RestMembers,
46
46
  type RestMePatch,
47
+ type RestPackConnect,
48
+ type RestPackConnectStarted,
47
49
  type RestPackTokenInput,
48
50
  type RestPeople,
49
51
  type RestPerson,
@@ -76,6 +78,9 @@ import {
76
78
  type RestVersion,
77
79
  type RestVersions,
78
80
  type RestWork,
81
+ type RestWorkspace,
82
+ type RestWorkspacePackConnect,
83
+ type RestWorkspacePatch,
79
84
  restArchivedTasksSchema,
80
85
  restCategoriesSchema,
81
86
  restCategorySchema,
@@ -96,6 +101,8 @@ import {
96
101
  restMemberSchema,
97
102
  restMembersSchema,
98
103
  restMeSchema,
104
+ restPackConnectSchema,
105
+ restPackConnectStartedSchema,
99
106
  restPeopleSchema,
100
107
  restPersonSchema,
101
108
  restPersonWorkSchema,
@@ -117,6 +124,8 @@ import {
117
124
  restVersionSchema,
118
125
  restVersionsSchema,
119
126
  restWorkSchema,
127
+ restWorkspacePackConnectSchema,
128
+ restWorkspaceSchema,
120
129
  } from "./schemas.js";
121
130
  import type { GoalState, TaskState } from "./vocabulary.js";
122
131
 
@@ -345,6 +354,12 @@ export interface ProjectClient {
345
354
  setPackToken(input: RestPackTokenInput): Promise<RestProject>;
346
355
  /** Takes the pack token away; for an owner. */
347
356
  removePackToken(): Promise<RestProject>;
357
+ /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
358
+ connectPackToken(): Promise<RestPackConnectStarted>;
359
+ /** How that connection stands; the token is kept when the person approved. For an owner. */
360
+ packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
361
+ /** Gives the connection up; for an owner. */
362
+ cancelPackTokenConnection(): Promise<void>;
348
363
  /** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
349
364
  rules(signal?: AbortSignal): Promise<RestProjectRules>;
350
365
  /** A folder's pages and folders, or the pages a search finds. */
@@ -380,7 +395,26 @@ export interface ConsoleClient {
380
395
  giveUpToken(): Promise<void>;
381
396
  /** Keeps the person's choice of language, or none; for a person signed in. */
382
397
  updateMe(patch: RestMePatch): Promise<RestMe>;
398
+ /** Changes the workspace's settings (ADR-0035); for an administrator, with a session or a token. */
399
+ updateWorkspace(patch: RestWorkspacePatch): Promise<RestWorkspace>;
400
+ /**
401
+ * The workspace's SkillCDN token (ADR-0036), which reads the organization's pack for every
402
+ * project that names no address and holds no token of its own; an administrator's. Set from
403
+ * a token made by hand, taken away, or connected with a code a person approves at SkillCDN,
404
+ * asked after until the token is kept or the request ended.
405
+ */
406
+ setWorkspacePackToken(input: RestPackTokenInput): Promise<RestWorkspace>;
407
+ removeWorkspacePackToken(): Promise<RestWorkspace>;
408
+ connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
409
+ workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
410
+ cancelWorkspacePackTokenConnection(): Promise<void>;
411
+ /** The people the asker may see: everyone, or those they share a project with (ADR-0035). */
383
412
  people(signal?: AbortSignal): Promise<RestPeople>;
413
+ /**
414
+ * One person of the workspace by their exact login, compared without regard to case, or
415
+ * nobody: what an owner lists someone by when the People page does not show them.
416
+ */
417
+ findPerson(login: string, signal?: AbortSignal): Promise<RestPerson | undefined>;
384
418
  /** Changes what a person is, or whether they are in; for an administrator. */
385
419
  updatePerson(id: string, patch: RestPersonPatch): Promise<RestPerson>;
386
420
  /** Lets a login in (ADR-0022): whoever signs in with that account becomes a person; for an administrator. */
@@ -766,6 +800,23 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
766
800
  request("PUT", `${base}${at()}/pack-token`, input, restProjectSchema),
767
801
  removePackToken: () =>
768
802
  request("DELETE", `${base}${at()}/pack-token`, undefined, restProjectSchema),
803
+ connectPackToken: () =>
804
+ request(
805
+ "POST",
806
+ `${base}${at()}/pack-token/connect`,
807
+ undefined,
808
+ restPackConnectStartedSchema,
809
+ ),
810
+ packTokenConnection: (signal) =>
811
+ request(
812
+ "GET",
813
+ `${base}${at()}/pack-token/connect`,
814
+ undefined,
815
+ restPackConnectSchema,
816
+ signal,
817
+ ),
818
+ cancelPackTokenConnection: () =>
819
+ request("DELETE", `${base}${at()}/pack-token/connect`, undefined, undefined),
769
820
  rules: (signal) =>
770
821
  request("GET", `${base}${at("rules")}`, undefined, restProjectRulesSchema, signal),
771
822
  documents: (filter = {}, signal) =>
@@ -829,6 +880,39 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
829
880
  giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
830
881
  people: (signal) =>
831
882
  request("GET", `${base}${REST_ROUTES.people}`, undefined, restPeopleSchema, signal),
883
+ findPerson: async (login, signal) => {
884
+ const found = await request(
885
+ "GET",
886
+ withQuery(REST_ROUTES.people, { login }),
887
+ undefined,
888
+ restPeopleSchema,
889
+ signal,
890
+ );
891
+ return found.items[0];
892
+ },
893
+ updateWorkspace: (patch) =>
894
+ request("PATCH", `${base}${REST_ROUTES.workspace}`, patch, restWorkspaceSchema),
895
+ setWorkspacePackToken: (input) =>
896
+ request("PUT", `${base}${REST_ROUTES.workspacePackToken}`, input, restWorkspaceSchema),
897
+ removeWorkspacePackToken: () =>
898
+ request("DELETE", `${base}${REST_ROUTES.workspacePackToken}`, undefined, restWorkspaceSchema),
899
+ connectWorkspacePackToken: () =>
900
+ request(
901
+ "POST",
902
+ `${base}${REST_ROUTES.workspacePackToken}/connect`,
903
+ undefined,
904
+ restPackConnectStartedSchema,
905
+ ),
906
+ workspacePackTokenConnection: (signal) =>
907
+ request(
908
+ "GET",
909
+ `${base}${REST_ROUTES.workspacePackToken}/connect`,
910
+ undefined,
911
+ restWorkspacePackConnectSchema,
912
+ signal,
913
+ ),
914
+ cancelWorkspacePackTokenConnection: () =>
915
+ request("DELETE", `${base}${REST_ROUTES.workspacePackToken}/connect`, undefined, undefined),
832
916
  updatePerson: (id, patch) =>
833
917
  request("PATCH", `${base}${restPath("people", id)}`, patch, restPersonSchema),
834
918
  createListing: (input) =>
@@ -5,11 +5,13 @@ import {
5
5
  type RestGoal,
6
6
  type RestPerson,
7
7
  type RestTask,
8
+ type WeekStart,
8
9
  } from "../api.js";
9
10
  import {
10
11
  addDays,
11
12
  addMonths,
12
13
  daysBetween,
14
+ deviceLanguage,
13
15
  firstDayOfWeek,
14
16
  formatDay,
15
17
  formatDays,
@@ -55,6 +57,8 @@ export interface CalendarProps {
55
57
  readonly goalHref?: ((goal: RestGoal) => string) | undefined;
56
58
  /** Where the calendar is on another month. */
57
59
  readonly monthHref: (month: string) => string;
60
+ /** The day the weeks begin on, as the workspace says (ADR-0035); the language's when left out. */
61
+ readonly weekStart?: WeekStart | undefined;
58
62
  }
59
63
 
60
64
  /** One task's stretch across one week: which columns it covers, and in which lane it lies. */
@@ -469,7 +473,7 @@ export function Calendar(props: CalendarProps) {
469
473
  const { tag } = useLanguage();
470
474
  const [pointed, point, leave] = usePointed();
471
475
  const now = props.today ?? todayOf();
472
- const firstDay = firstDayOfWeek(tag);
476
+ const firstDay = firstDayOfWeek(deviceLanguage(tag), props.weekStart);
473
477
  const weeks = monthGrid(props.month, firstDay);
474
478
  const on = onDays(props.tasks);
475
479
  const goalsOn = props.goalHref === undefined ? undefined : goalsOnDays(props.goals ?? []);
@@ -567,6 +571,8 @@ export interface WeekProps {
567
571
  readonly weekHref: (day: string) => string;
568
572
  /** What stands before a task's title where the tasks are of several projects: the project's mark. */
569
573
  readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
574
+ /** The day the week begins on, as the workspace says (ADR-0035); the language's when left out. */
575
+ readonly weekStart?: WeekStart | undefined;
570
576
  }
571
577
 
572
578
  /** One week on its own: the days as columns, the tasks on them, and the weeks around a step away. */
@@ -575,7 +581,7 @@ export function Week(props: WeekProps) {
575
581
  const { tag } = useLanguage();
576
582
  const [pointed, point, leave] = usePointed();
577
583
  const now = props.today ?? todayOf();
578
- const firstDay = firstDayOfWeek(tag);
584
+ const firstDay = firstDayOfWeek(deviceLanguage(tag), props.weekStart);
579
585
  const days = weekOf(props.day ?? now, firstDay);
580
586
  const first = days[0] ?? now;
581
587
  const last = days[days.length - 1] ?? now;
@@ -46,6 +46,8 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
46
46
  return feed.event.personDeactivated(login);
47
47
  case "person.reactivated":
48
48
  return feed.event.personReactivated(login);
49
+ case "workspace.updated":
50
+ return feed.event.workspaceUpdated(fields);
49
51
  case "project.created":
50
52
  return feed.event.projectCreated(project);
51
53
  case "project.updated":
@@ -0,0 +1,179 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { PackConnectEnding, RestPackConnect, RestPackConnectStarted } from "../api.js";
3
+ import { useWords } from "../i18n/index.js";
4
+ import { Button, Time } from "./ui.js";
5
+
6
+ // A private pack connected with a code a person approves (ADR-0036): an owner, or for the
7
+ // workspace's token an administrator, asks, the console asks the deployment and shows what it
8
+ // answered, the code and the page, and the person approves there; the page asks the console
9
+ // again at the interval the deployment named until the token is kept or the request ended.
10
+ // Takes its data through the callbacks and keeps nothing itself.
11
+
12
+ /**
13
+ * How a connection stands, as the component needs it: what the API answers for a project's
14
+ * token and for the workspace's both say this much, with what was connected beside it.
15
+ */
16
+ export type PackConnectState =
17
+ | Extract<RestPackConnect, { status: "none" | "pending" | "ended" }>
18
+ | { readonly status: "connected" };
19
+
20
+ export interface PackConnectProps {
21
+ /** Asks the deployment to connect; answers what the person is shown. */
22
+ readonly onStart: () => Promise<RestPackConnectStarted>;
23
+ /** How the connection stands; the token is kept when the person approved. */
24
+ readonly poll: (signal?: AbortSignal) => Promise<PackConnectState>;
25
+ /** Gives the connection up. */
26
+ readonly onCancel: () => Promise<void>;
27
+ /** Called once the token is kept. */
28
+ readonly onConnected?: (() => void) | undefined;
29
+ readonly busy?: boolean | undefined;
30
+ }
31
+
32
+ type Shown =
33
+ | { readonly kind: "idle" }
34
+ | {
35
+ readonly kind: "waiting";
36
+ readonly code: string;
37
+ readonly url: string;
38
+ readonly expiresAt: string;
39
+ readonly intervalSeconds: number;
40
+ }
41
+ | { readonly kind: "ended"; readonly reason: PackConnectEnding }
42
+ | { readonly kind: "connected" };
43
+
44
+ /** The least often the console is asked again, whatever the deployment says. */
45
+ const MIN_INTERVAL_SECONDS = 2;
46
+
47
+ export function PackConnect(props: PackConnectProps) {
48
+ const words = useWords();
49
+ const pack = words.projects.packToken;
50
+ const { poll, onConnected } = props;
51
+ const [shown, setShown] = useState<Shown>({ kind: "idle" });
52
+ const [problem, setProblem] = useState<string | undefined>(undefined);
53
+
54
+ // A request begun before the page was opened is picked up where it stands.
55
+ useEffect(() => {
56
+ const controller = new AbortController();
57
+ poll(controller.signal)
58
+ .then((state) => {
59
+ if (!controller.signal.aborted && state.status === "pending") {
60
+ setShown({ kind: "waiting", ...state });
61
+ }
62
+ })
63
+ .catch(() => undefined);
64
+ return () => controller.abort();
65
+ }, [poll]);
66
+
67
+ // While waiting, the console is asked again at the interval the deployment named; a new
68
+ // answer, pending still, is a new state, which schedules the next asking.
69
+ useEffect(() => {
70
+ if (shown.kind !== "waiting") {
71
+ return;
72
+ }
73
+ const controller = new AbortController();
74
+ const timer = setTimeout(() => {
75
+ poll(controller.signal)
76
+ .then((state) => {
77
+ if (controller.signal.aborted) {
78
+ return;
79
+ }
80
+ setProblem(undefined);
81
+ if (state.status === "pending") {
82
+ setShown({ kind: "waiting", ...state });
83
+ } else if (state.status === "connected") {
84
+ setShown({ kind: "connected" });
85
+ onConnected?.();
86
+ } else if (state.status === "ended") {
87
+ setShown({ kind: "ended", reason: state.reason });
88
+ } else {
89
+ setShown({ kind: "idle" });
90
+ }
91
+ })
92
+ .catch((failure: unknown) => {
93
+ if (controller.signal.aborted) {
94
+ return;
95
+ }
96
+ // The console could not be asked this time; it is asked again.
97
+ setProblem(failure instanceof Error ? failure.message : String(failure));
98
+ setShown((current) => (current.kind === "waiting" ? { ...current } : current));
99
+ });
100
+ }, Math.max(shown.intervalSeconds, MIN_INTERVAL_SECONDS) * 1000);
101
+ return () => {
102
+ controller.abort();
103
+ clearTimeout(timer);
104
+ };
105
+ }, [shown, poll, onConnected]);
106
+
107
+ const start = () => {
108
+ setProblem(undefined);
109
+ props
110
+ .onStart()
111
+ .then((started) => setShown({ kind: "waiting", ...started }))
112
+ .catch((failure: unknown) => {
113
+ setProblem(failure instanceof Error ? failure.message : String(failure));
114
+ });
115
+ };
116
+ const cancel = () => {
117
+ props
118
+ .onCancel()
119
+ .then(() => setShown({ kind: "idle" }))
120
+ .catch((failure: unknown) => {
121
+ setProblem(failure instanceof Error ? failure.message : String(failure));
122
+ });
123
+ };
124
+
125
+ return (
126
+ <div className="sc-pack-connect" aria-live="polite">
127
+ {shown.kind === "waiting" ? (
128
+ <>
129
+ <p className="sc-pack-connect-code">
130
+ <span className="sc-field-label">{pack.connectCode}</span>
131
+ <code className="sc-pack-code">{shown.code}</code>
132
+ </p>
133
+ <div className="sc-form-actions">
134
+ <a
135
+ className="sc-button sc-button-primary"
136
+ href={shown.url}
137
+ target="_blank"
138
+ rel="noopener noreferrer"
139
+ >
140
+ {pack.connectOpen}
141
+ </a>
142
+ <Button variant="ghost" disabled={props.busy === true} onClick={cancel}>
143
+ {pack.connectCancel}
144
+ </Button>
145
+ </div>
146
+ <p className="sc-pack-connect-waiting">
147
+ <span className="sc-spinner" aria-hidden="true">
148
+ <span className="sc-spinner-ring" />
149
+ </span>
150
+ <span>
151
+ {pack.connectWaiting} {pack.connectExpires} <Time iso={shown.expiresAt} />
152
+ </span>
153
+ </p>
154
+ </>
155
+ ) : shown.kind === "connected" ? (
156
+ <p>{pack.connectDone}</p>
157
+ ) : (
158
+ <>
159
+ <p className="sc-field-hint">{pack.connectHint}</p>
160
+ {shown.kind === "ended" && (
161
+ <p className="sc-form-error" role="alert">
162
+ {pack.connectEnded[shown.reason]}
163
+ </p>
164
+ )}
165
+ <div className="sc-form-actions">
166
+ <Button variant="primary" disabled={props.busy === true} onClick={start}>
167
+ {pack.connect}
168
+ </Button>
169
+ </div>
170
+ </>
171
+ )}
172
+ {problem !== undefined && (
173
+ <p className="sc-form-error" role="alert">
174
+ {problem}
175
+ </p>
176
+ )}
177
+ </div>
178
+ );
179
+ }
@@ -0,0 +1,74 @@
1
+ import type { RestPackConnectStarted } from "../api.js";
2
+ import { useWords } from "../i18n/index.js";
3
+ import { PackConnect, type PackConnectState } from "./pack-connect.js";
4
+ import { PackTokenForm } from "./projects.js";
5
+ import { Button } from "./ui.js";
6
+
7
+ // The SkillCDN token a private pack is read with (ADR-0028, ADR-0036), as a project's Settings
8
+ // page and the workspace's offer it: what the token is for, then the two ways to one side by
9
+ // side, a code approved at SkillCDN and a token made there by hand. Takes its data through the
10
+ // callbacks and keeps nothing itself; whose token it is, the page says in the title and the intro.
11
+
12
+ export interface PackTokenWaysProps {
13
+ /** What the section is called, and what assistive technology reads it as. */
14
+ readonly title: string;
15
+ /** What the token is for, in the words of the page it is on. */
16
+ readonly intro: string;
17
+ /** Whether a token is held now, and since when. */
18
+ readonly set: boolean;
19
+ readonly setAt?: string | undefined;
20
+ readonly busy?: boolean | undefined;
21
+ /** Asks the deployment to connect; answers what the person is shown. */
22
+ readonly onStart: () => Promise<RestPackConnectStarted>;
23
+ /** How the connection stands; the token is kept when the person approved. */
24
+ readonly poll: (signal?: AbortSignal) => Promise<PackConnectState>;
25
+ /** Gives the connection up. */
26
+ readonly onCancelConnect: () => Promise<void>;
27
+ /** Called once the token is kept. */
28
+ readonly onConnected?: (() => void) | undefined;
29
+ /** Called with a token made by hand, as typed or pasted. */
30
+ readonly onSubmit: (token: string) => void;
31
+ readonly onRemove?: (() => void) | undefined;
32
+ /** Leaves the page without changing anything. */
33
+ readonly onBack?: (() => void) | undefined;
34
+ }
35
+
36
+ export function PackTokenWays(props: PackTokenWaysProps) {
37
+ const words = useWords();
38
+ const pack = words.projects.packToken;
39
+ return (
40
+ <section className="sc-panel sc-panel-form" aria-label={props.title}>
41
+ <h2 className="sc-section-title">{props.title}</h2>
42
+ <p className="sc-pack-intro">{props.intro}</p>
43
+ <div className="sc-pack-ways">
44
+ <section className="sc-pack-way" aria-label={pack.connectTitle}>
45
+ <h3 className="sc-pack-way-title">{pack.connectTitle}</h3>
46
+ <PackConnect
47
+ onStart={props.onStart}
48
+ poll={props.poll}
49
+ onCancel={props.onCancelConnect}
50
+ onConnected={props.onConnected}
51
+ busy={props.busy}
52
+ />
53
+ </section>
54
+ <section className="sc-pack-way" aria-label={pack.pasteTitle}>
55
+ <h3 className="sc-pack-way-title">{pack.pasteTitle}</h3>
56
+ <PackTokenForm
57
+ set={props.set}
58
+ setAt={props.setAt}
59
+ busy={props.busy}
60
+ onSubmit={props.onSubmit}
61
+ onRemove={props.onRemove}
62
+ />
63
+ </section>
64
+ </div>
65
+ {props.onBack !== undefined && (
66
+ <div className="sc-form-actions sc-pack-actions">
67
+ <Button variant="ghost" onClick={props.onBack}>
68
+ {words.common.cancel}
69
+ </Button>
70
+ </div>
71
+ )}
72
+ </section>
73
+ );
74
+ }