@checkstack/announcement-common 0.2.0
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.
- package/CHANGELOG.md +14 -0
- package/package.json +29 -0
- package/src/access.ts +20 -0
- package/src/index.ts +23 -0
- package/src/plugin-metadata.ts +9 -0
- package/src/routes.ts +8 -0
- package/src/rpc-contract.ts +99 -0
- package/src/schemas.ts +87 -0
- package/src/signals.ts +14 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @checkstack/announcement-common
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dee86ec: feat: add portal announcement system
|
|
8
|
+
|
|
9
|
+
Introduces a complete announcement system for communicating with portal users:
|
|
10
|
+
|
|
11
|
+
- **announcement-common**: Zod schemas for announcements (severity, visibility, display mode), oRPC contract with 6 procedures (public retrieval, user dismissal, admin CRUD), access rules, and `ANNOUNCEMENT_UPDATED` signal definition
|
|
12
|
+
- **announcement-backend**: Drizzle schema with `announcements` and `announcement_dismissals` tables, router with temporal filtering, visibility control, per-user dismissal persistence, user cleanup hook, real-time signal broadcasting on create/update/delete, and command palette registration ("Create Announcement", "Manage Announcements" with `⇧⌘A` shortcut)
|
|
13
|
+
- **announcement-frontend**: Admin management page with create/edit dialog, global banner component above the navbar (severity-colored, expandable markdown), dashboard cards with compact expand/collapse, admin menu link, and real-time WebSocket signal subscription for instant UI updates
|
|
14
|
+
- **frontend**: Integrates AnnouncementBanner into App.tsx for global visibility
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/announcement-common",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./src/index.ts"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@checkstack/common": "0.6.4",
|
|
12
|
+
"@checkstack/signal-common": "0.1.8",
|
|
13
|
+
"@orpc/contract": "^1.13.14",
|
|
14
|
+
"zod": "^4.2.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.7.2",
|
|
18
|
+
"@checkstack/tsconfig": "0.0.4",
|
|
19
|
+
"@checkstack/scripts": "0.1.2"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"lint": "bun run lint:code",
|
|
24
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
25
|
+
},
|
|
26
|
+
"checkstack": {
|
|
27
|
+
"type": "common"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/access.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { access } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Access rules for the Announcement plugin.
|
|
5
|
+
* Announcements don't use instance-level access (no teams/resource filtering).
|
|
6
|
+
* Only a single "manage" rule is needed — reading active announcements is public.
|
|
7
|
+
*/
|
|
8
|
+
export const announcementAccess = {
|
|
9
|
+
/** Manage announcements — create, edit, and delete. Admin-only. */
|
|
10
|
+
manage: access(
|
|
11
|
+
"announcement",
|
|
12
|
+
"manage",
|
|
13
|
+
"Manage announcements — create, edit, and delete",
|
|
14
|
+
),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* All access rules for registration with the plugin system.
|
|
19
|
+
*/
|
|
20
|
+
export const announcementAccessRules = [announcementAccess.manage];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { announcementAccess, announcementAccessRules } from "./access";
|
|
2
|
+
export {
|
|
3
|
+
announcementContract,
|
|
4
|
+
AnnouncementApi,
|
|
5
|
+
type AnnouncementContract,
|
|
6
|
+
} from "./rpc-contract";
|
|
7
|
+
export {
|
|
8
|
+
AnnouncementSeverityEnum,
|
|
9
|
+
AnnouncementVisibilityEnum,
|
|
10
|
+
AnnouncementDisplayModeEnum,
|
|
11
|
+
AnnouncementSchema,
|
|
12
|
+
CreateAnnouncementInputSchema,
|
|
13
|
+
UpdateAnnouncementInputSchema,
|
|
14
|
+
type AnnouncementSeverity,
|
|
15
|
+
type AnnouncementVisibility,
|
|
16
|
+
type AnnouncementDisplayMode,
|
|
17
|
+
type Announcement,
|
|
18
|
+
type CreateAnnouncementInput,
|
|
19
|
+
type UpdateAnnouncementInput,
|
|
20
|
+
} from "./schemas";
|
|
21
|
+
export * from "./plugin-metadata";
|
|
22
|
+
export { announcementRoutes } from "./routes";
|
|
23
|
+
export { ANNOUNCEMENT_UPDATED } from "./signals";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { definePluginMetadata } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin metadata for the announcement plugin.
|
|
5
|
+
* Exported from the common package so both backend and frontend can reference it.
|
|
6
|
+
*/
|
|
7
|
+
export const pluginMetadata = definePluginMetadata({
|
|
8
|
+
pluginId: "announcement",
|
|
9
|
+
});
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createClientDefinition, proc } from "@checkstack/common";
|
|
3
|
+
import { announcementAccess } from "./access";
|
|
4
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
5
|
+
import {
|
|
6
|
+
AnnouncementSchema,
|
|
7
|
+
CreateAnnouncementInputSchema,
|
|
8
|
+
UpdateAnnouncementInputSchema,
|
|
9
|
+
} from "./schemas";
|
|
10
|
+
|
|
11
|
+
export const announcementContract = {
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Public endpoints
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get currently active announcements.
|
|
18
|
+
* Public endpoint — anyone can read active announcements.
|
|
19
|
+
* If the caller is authenticated, dismissed announcements are excluded.
|
|
20
|
+
*/
|
|
21
|
+
getActiveAnnouncements: proc({
|
|
22
|
+
operationType: "query",
|
|
23
|
+
userType: "public",
|
|
24
|
+
access: [],
|
|
25
|
+
})
|
|
26
|
+
.input(
|
|
27
|
+
z
|
|
28
|
+
.object({
|
|
29
|
+
/** If true, include dismissed announcements in the result. */
|
|
30
|
+
includeDismissed: z.boolean().optional(),
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
33
|
+
)
|
|
34
|
+
.output(z.object({ announcements: z.array(AnnouncementSchema) })),
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Authenticated user endpoints
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dismiss an announcement for the current user.
|
|
42
|
+
* Records a server-side dismissal so the announcement won't appear again.
|
|
43
|
+
*/
|
|
44
|
+
dismissAnnouncement: proc({
|
|
45
|
+
operationType: "mutation",
|
|
46
|
+
userType: "user",
|
|
47
|
+
access: [],
|
|
48
|
+
})
|
|
49
|
+
.input(z.object({ announcementId: z.string() }))
|
|
50
|
+
.output(z.void()),
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Admin endpoints
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/** List all announcements regardless of active/temporal state (admin view). */
|
|
57
|
+
listAllAnnouncements: proc({
|
|
58
|
+
operationType: "query",
|
|
59
|
+
userType: "authenticated",
|
|
60
|
+
access: [announcementAccess.manage],
|
|
61
|
+
}).output(z.object({ announcements: z.array(AnnouncementSchema) })),
|
|
62
|
+
|
|
63
|
+
/** Create a new announcement. */
|
|
64
|
+
createAnnouncement: proc({
|
|
65
|
+
operationType: "mutation",
|
|
66
|
+
userType: "authenticated",
|
|
67
|
+
access: [announcementAccess.manage],
|
|
68
|
+
})
|
|
69
|
+
.input(CreateAnnouncementInputSchema)
|
|
70
|
+
.output(AnnouncementSchema),
|
|
71
|
+
|
|
72
|
+
/** Update an existing announcement. */
|
|
73
|
+
updateAnnouncement: proc({
|
|
74
|
+
operationType: "mutation",
|
|
75
|
+
userType: "authenticated",
|
|
76
|
+
access: [announcementAccess.manage],
|
|
77
|
+
})
|
|
78
|
+
.input(UpdateAnnouncementInputSchema)
|
|
79
|
+
.output(AnnouncementSchema),
|
|
80
|
+
|
|
81
|
+
/** Delete an announcement permanently. */
|
|
82
|
+
deleteAnnouncement: proc({
|
|
83
|
+
operationType: "mutation",
|
|
84
|
+
userType: "authenticated",
|
|
85
|
+
access: [announcementAccess.manage],
|
|
86
|
+
})
|
|
87
|
+
.input(z.object({ id: z.string() }))
|
|
88
|
+
.output(z.object({ success: z.boolean() })),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Export contract type
|
|
92
|
+
export type AnnouncementContract = typeof announcementContract;
|
|
93
|
+
|
|
94
|
+
// Export client definition for type-safe forPlugin usage
|
|
95
|
+
// Use: const client = rpcApi.forPlugin(AnnouncementApi);
|
|
96
|
+
export const AnnouncementApi = createClientDefinition(
|
|
97
|
+
announcementContract,
|
|
98
|
+
pluginMetadata,
|
|
99
|
+
);
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Announcement severity determines the visual styling of the announcement.
|
|
5
|
+
* - info: Blue/primary accent — general information
|
|
6
|
+
* - warning: Amber — important notice
|
|
7
|
+
* - critical: Red/destructive — urgent alert
|
|
8
|
+
*/
|
|
9
|
+
export const AnnouncementSeverityEnum = z.enum(["info", "warning", "critical"]);
|
|
10
|
+
export type AnnouncementSeverity = z.infer<typeof AnnouncementSeverityEnum>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Controls who can see the announcement.
|
|
14
|
+
* - all: Everyone, including unauthenticated visitors
|
|
15
|
+
* - authenticated: Only logged-in users
|
|
16
|
+
*/
|
|
17
|
+
export const AnnouncementVisibilityEnum = z.enum(["all", "authenticated"]);
|
|
18
|
+
export type AnnouncementVisibility = z.infer<typeof AnnouncementVisibilityEnum>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Controls where the announcement is displayed.
|
|
22
|
+
* - banner: Global strip above the navbar on every page
|
|
23
|
+
* - dashboard: Card in the dashboard Overview section
|
|
24
|
+
* - both: Displayed in both locations
|
|
25
|
+
*/
|
|
26
|
+
export const AnnouncementDisplayModeEnum = z.enum([
|
|
27
|
+
"banner",
|
|
28
|
+
"dashboard",
|
|
29
|
+
"both",
|
|
30
|
+
]);
|
|
31
|
+
export type AnnouncementDisplayMode = z.infer<
|
|
32
|
+
typeof AnnouncementDisplayModeEnum
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Core announcement entity schema.
|
|
37
|
+
*/
|
|
38
|
+
export const AnnouncementSchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
title: z.string(),
|
|
41
|
+
message: z.string(),
|
|
42
|
+
severity: AnnouncementSeverityEnum,
|
|
43
|
+
visibility: AnnouncementVisibilityEnum,
|
|
44
|
+
displayMode: AnnouncementDisplayModeEnum,
|
|
45
|
+
active: z.boolean(),
|
|
46
|
+
startsAt: z.date().optional(),
|
|
47
|
+
expiresAt: z.date().optional(),
|
|
48
|
+
createdBy: z.string(),
|
|
49
|
+
createdAt: z.date(),
|
|
50
|
+
updatedAt: z.date(),
|
|
51
|
+
});
|
|
52
|
+
export type Announcement = z.infer<typeof AnnouncementSchema>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Input schema for creating an announcement.
|
|
56
|
+
*/
|
|
57
|
+
export const CreateAnnouncementInputSchema = z.object({
|
|
58
|
+
title: z.string().min(1, "Title is required"),
|
|
59
|
+
message: z.string().min(1, "Message is required"),
|
|
60
|
+
severity: AnnouncementSeverityEnum,
|
|
61
|
+
visibility: AnnouncementVisibilityEnum,
|
|
62
|
+
displayMode: AnnouncementDisplayModeEnum,
|
|
63
|
+
active: z.boolean().optional().default(true),
|
|
64
|
+
startsAt: z.date().optional(),
|
|
65
|
+
expiresAt: z.date().optional(),
|
|
66
|
+
});
|
|
67
|
+
export type CreateAnnouncementInput = z.infer<
|
|
68
|
+
typeof CreateAnnouncementInputSchema
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Input schema for updating an announcement.
|
|
73
|
+
*/
|
|
74
|
+
export const UpdateAnnouncementInputSchema = z.object({
|
|
75
|
+
id: z.string(),
|
|
76
|
+
title: z.string().min(1).optional(),
|
|
77
|
+
message: z.string().min(1).optional(),
|
|
78
|
+
severity: AnnouncementSeverityEnum.optional(),
|
|
79
|
+
visibility: AnnouncementVisibilityEnum.optional(),
|
|
80
|
+
displayMode: AnnouncementDisplayModeEnum.optional(),
|
|
81
|
+
active: z.boolean().optional(),
|
|
82
|
+
startsAt: z.date().optional(),
|
|
83
|
+
expiresAt: z.date().optional(),
|
|
84
|
+
});
|
|
85
|
+
export type UpdateAnnouncementInput = z.infer<
|
|
86
|
+
typeof UpdateAnnouncementInputSchema
|
|
87
|
+
>;
|
package/src/signals.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createSignal } from "@checkstack/signal-common";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Broadcast when an announcement is created, updated, or deleted.
|
|
6
|
+
* Frontend components listening to this signal should refetch active announcements.
|
|
7
|
+
*/
|
|
8
|
+
export const ANNOUNCEMENT_UPDATED = createSignal(
|
|
9
|
+
"announcement.updated",
|
|
10
|
+
z.object({
|
|
11
|
+
announcementId: z.string(),
|
|
12
|
+
action: z.enum(["created", "updated", "deleted"]),
|
|
13
|
+
}),
|
|
14
|
+
);
|