feat(platform): Phase 446 moderation spans + guildBans view (v0.8.409)

Extend ban/kick/timeout/audit.export spans with guildId metadata; add
automod.setConfig span and error logging; expose guildBans in view snapshot.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-23 22:44:54 -04:00
co-authored by Cursor
parent 064cda4b59
commit 17930ea6d4
2 changed files with 51 additions and 17 deletions
+3
View File
@@ -68,6 +68,7 @@ Expose a single `EventEmitter` API the UI drives over IPC (`apps/pearcord/index.
| Property | Role |
|----------|------|
| `mode` | `'home'`, `'guild'`, or DM mode |
| `guildBans` | Ban rows for active guild (`@pearcord/bans`, limit 200) (v0.8.409) |
| `getViewState()` / snapshot builders | IPC `state` payloads for UI |
| `_readOnly`, `_companionMode` | Companion restrictions |
@@ -149,6 +150,8 @@ Primary consumer: `apps/pearcord/index.js` sidecar reading pear-pipe JSON.
**v0.8.407:** `markNotificationRead(notificationId)` wraps inbox `markRead` with `notification.markRead` span + `notification.markRead error`. `markAllNotificationsRead` span ends with `count` + `guildId`. `markChannelRead` logs `inbox.mark-read` span + error for non-thread channels (thread channels use `thread.read`). Smokes: `test:notification-platform-errors`, `test:inbox-platform-errors`. Bundle: `test:phase444-notifications-inbox`.
**v0.8.409 (Phase 446):** `banMember`/`kickMember`/`timeoutMember` spans include `guildId` metadata on start and error lines; `exportAuditLog` span includes `guildId`, `format`, `filter`; `setAutomodConfig` logs `automod.setConfig` span + `automod.setConfig error`. View snapshot adds `guildBans` (`db.find(COLLECTIONS.BANS, { guildId }, { limit: 200 })`). Bundle: `test:phase446-moderation-audit`.
**v0.8.408:** `markChannelRead` calls `_invalidateSearchCache()` when the channel is a thread (`thread.read` path) so search results refresh after thread read. Smoke: `test:search-cache-invalidate-thread-read`. Bundle: `test:phase445-search-inbox`.
**v0.8.406:** `createThread`/`createForumPost`/`markChannelRead` (thread channels)/`archiveThread` log `thread.create`/`forum.post`/`thread.read`/`thread.archive` spans and errors; audit entries for `thread.create`, `forum.post.create`, `thread.archive`, and `thread.unarchive`. Smoke: `test:thread-platform-errors`.
+48 -17
View File
@@ -3659,7 +3659,12 @@ class PearcordPlatform extends EventEmitter {
}
async exportAuditLog (opts = {}) {
const span = this.log.time('audit.export', { format: opts.format || 'json' })
const guildId = this.guild?.guild?.id || null
const span = this.log.time('audit.export', {
format: opts.format || 'json',
guildId,
filter: opts.filter || 'all'
})
try {
if (!this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
@@ -3743,6 +3748,8 @@ class PearcordPlatform extends EventEmitter {
} catch (err) {
this.log.error('audit.export error', {
format: opts.format || 'json',
guildId: this.guild?.guild?.id || null,
filter: opts.filter || 'all',
error: err?.message || String(err)
})
span.fail(err)
@@ -7405,7 +7412,8 @@ class PearcordPlatform extends EventEmitter {
}
async timeoutMember ({ userId, durationMinutes, reason }) {
const span = this.log.time('timeout.member', { userId })
const guildId = this.guild?.guild?.id || null
const span = this.log.time('timeout.member', { userId, guildId, durationMinutes })
try {
const user = this.identity.user
if (!user || !this.guild?.guild) throw new Error('no guild')
@@ -7425,11 +7433,13 @@ class PearcordPlatform extends EventEmitter {
await this._audit('member.timeout', { guildId: this.guild.guild.id, targetId: userId })
this.guild.gossipTimeout(row)
this.emit('timeout', row)
span.end({ userId })
span.end({ userId, guildId, durationMinutes })
return row
} catch (err) {
this.log.error('timeout.member error', {
userId,
guildId,
durationMinutes,
error: err?.message || String(err)
})
span.fail(err)
@@ -8641,17 +8651,29 @@ class PearcordPlatform extends EventEmitter {
}
async setAutomodConfig (partial) {
if (!this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to edit automod')
const guildId = this.guild?.guild?.id || null
const span = this.log.time('automod.setConfig', { guildId })
try {
if (!this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to edit automod')
}
await this._ensureGuildModules()
const config = await this.automod.setConfig(partial)
this.guild.gossipAutomodConfig(config)
await this._audit('automod.update', { guildId: config.guildId })
this.emit('automod', config)
span.end({ guildId, enabled: !!config.enabled })
return config
} catch (err) {
this.log.error('automod.setConfig error', {
guildId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
await this._ensureGuildModules()
const config = await this.automod.setConfig(partial)
this.guild.gossipAutomodConfig(config)
await this._audit('automod.update', { guildId: config.guildId })
this.emit('automod', config)
return config
}
async dryRunAutomodMessage (content, opts = {}) {
@@ -10831,7 +10853,8 @@ class PearcordPlatform extends EventEmitter {
}
async kickMember ({ userId, reason }) {
const span = this.log.time('kick.member', { userId })
const guildId = this.guild?.guild?.id || null
const span = this.log.time('kick.member', { userId, guildId })
try {
const user = this.identity.user
if (!user || !this.guild?.guild) throw new Error('no guild')
@@ -10856,11 +10879,12 @@ class PearcordPlatform extends EventEmitter {
await this._audit('member.kick', { guildId: payload.guildId, targetId: userId })
this.guild.gossipKick(payload)
this.emit('kick', payload)
span.end({ userId })
span.end({ userId, guildId })
return payload
} catch (err) {
this.log.error('kick.member error', {
userId,
guildId,
error: err?.message || String(err)
})
span.fail(err)
@@ -10869,7 +10893,8 @@ class PearcordPlatform extends EventEmitter {
}
async banMember ({ userId, reason }) {
const span = this.log.time('ban.member', { userId })
const guildId = this.guild?.guild?.id || null
const span = this.log.time('ban.member', { userId, guildId })
try {
const user = this.identity.user
if (!user || !this.guild?.guild) throw new Error('no guild')
@@ -10895,11 +10920,12 @@ class PearcordPlatform extends EventEmitter {
await this._audit('member.ban', { guildId: this.guild.guild.id, targetId: userId })
this.guild.gossipBan(ban)
this.emit('ban', ban)
span.end({ userId })
span.end({ userId, guildId })
return ban
} catch (err) {
this.log.error('ban.member error', {
userId,
guildId,
error: err?.message || String(err)
})
span.fail(err)
@@ -11185,6 +11211,10 @@ class PearcordPlatform extends EventEmitter {
communicationDisabled = await this.moderation.isTimedOut(guild.id, user.id)
}
}
let guildBans = []
if (guild && this.mode === 'guild') {
guildBans = (await this.db.find(COLLECTIONS.BANS, { guildId: guild.id }, { limit: 200 })) || []
}
const memberNameById = {}
if (guild) {
for (const mem of members) {
@@ -11734,6 +11764,7 @@ class PearcordPlatform extends EventEmitter {
auditLog,
attachmentsByMessage,
memberTimeouts,
guildBans,
communicationDisabled,
voiceStatesByChannel,
stageRolesByChannel,