Continuous Feedback (FBCK)
Peer feedback · 1:1 meeting notes · formal HR memos · recognition wall · real-time SignalR push — unified continuous feedback culture
Vision
FBCK moves GoodBooks from annual appraisal-only feedback to a culture of continuous communication. It combines four previously siloed capabilities — peer feedback requests, structured 1:1 meeting notes, formal HR/manager memos, and a social recognition wall — into one unified module with real-time SignalR push for every interaction.
Four Sub-Systems
Peer, manager, upward, self, and 360 feedback requests. Configurable questions JSON. Can be linked to an appraisal or goal context.
Structured 1:1 meeting notes with agenda, notes, and action item creation. Dual-side: employee and manager each contribute.
Formal HR/manager communications: commendation, disciplinary, informational, policy. Confidential memos hidden until acknowledged.
Appreciation, kudos, awards, spot bonuses. Public recognitions visible on tenant-wide recognition wall. Immutable once created.
Key Concepts Glossary
| Term | Definition |
|---|---|
| FeedbackType | 0=Peer, 1=Manager, 2=Self, 3=Upward, 4=360. Determines who is giving feedback about whom. |
| ContextType | 0=Standalone (independent feedback), 1=AppraisalLinked (tied to a TAPPRAISAL), 2=GoalLinked (tied to a TGOAL). |
| ContextId | The AppraisalId or GoalId when ContextType > 0. Stored as -1 when Standalone. |
| QuestionsJson / ResponsesJson | Flexible JSON arrays for feedback questions and answers. Allows custom question sets per feedback request without schema changes. |
| MemoType | 0=Commendation, 1=Disciplinary, 2=Informational, 3=PolicyUpdate. Determines icon, color, and HR access level. |
| IsConfidential | Bit on TMEMO. Confidential memos are hidden from ESS until Status=2 (Acknowledged). Used for disciplinary memos where HR must confirm delivery before content is exposed. |
| RecognitionType | 0=Appreciation, 1=Kudos, 2=Award, 3=SpotBonus. Drives display style and notification urgency. |
| IsPublic | Bit on TRECOGNITION. Public recognitions appear in the tenant-wide recognition wall feed; private ones visible only to recipient. |
| FeedbackSummaryDTO | Lean 4-field DTO used by PERM's GetMyPerformanceSummary to pull feedback counts across modules without loading full records. |
| HubGroup: fbck:user:{userId} | Personal notifications — memos, direct feedback, private recognitions. |
| HubGroup: fbck:client:{clientId} | Tenant-wide recognition wall feed — public recognitions. |
| Dual Group Membership | Every connected user joins BOTH groups. Group cleanup must happen in OnDisconnectedAsync for both. |
Database Schema
TFEEDBACK — Feedback Requests
| Column | Type | Notes |
|---|---|---|
| FeedbackId | int PK | |
| RequestedById / TargetEmployeeId / ResponderId | int | Who requested, subject, who responds |
| FeedbackType | smallint | 0=Peer 1=Manager 2=Self 3=Upward 4=360 |
| ContextType / ContextId | smallint / int | 0=Standalone; 1=AppraisalLinked; 2=GoalLinked. ContextId=-1 when Standalone. |
| QuestionsJson / ResponsesJson | nvarchar(max) | JSON arrays — flexible question sets per request |
| OverallRating | decimal(5,2) | 0 = not rated |
| Status | smallint | 0=Requested 1=InProgress 2=Submitted 3=Declined |
| DueDate | date nullable | |
| TenantId | int | Indexes: (TargetEmployeeId, TenantId, Status), (ResponderId, TenantId, Status) |
TONONOTE — 1:1 Meeting Notes
| Column | Type | Notes |
|---|---|---|
| TONONoteId / EmployeeId / ManagerId | int | |
| ScheduledDate / ActualDatetime | date / datetime | ActualDatetime set when meeting actually held |
| AgendaJson | nvarchar(max) | JSON array of agenda topics pre-meeting |
| NotesJson | nvarchar(max) | JSON notes captured during/after meeting |
| Status | smallint | 0=Scheduled, 1=Completed, 2=Cancelled |
| TenantId | int | Indexes: (EmployeeId, TenantId, Status), (ManagerId, TenantId, ScheduledDate) |
TACTIONITEM — Action Items
| Column | Type | Notes |
|---|---|---|
| ActionItemId | int PK | |
| ContextType / ContextId | smallint / int | 0=OneOnOne(TONONoteId), 1=Feedback(FeedbackId), 2=Standalone(-1) |
| EmployeeId / OwnerId | int | Who must complete / who created it |
| ActionText / DueDate | nvarchar / date | |
| Status | smallint | 0=Pending 1=Completed 2=Overdue |
| CompletionRemarks | nvarchar nullable | Required on completion |
| TenantId | int | Indexes: (EmployeeId, TenantId, Status), (DueDate, TenantId, Status) |
TMEMO — Formal HR/Manager Memos
| Column | Type | Notes |
|---|---|---|
| MemoId / FromEmployeeId / ToEmployeeId | int | |
| MemoType | smallint | 0=Commendation 1=Disciplinary 2=Informational 3=PolicyUpdate |
| Subject / ContentHtml | nvarchar | Rich HTML content |
| IsConfidential | bit | When true: hidden from ESS until Status=2 |
| Status | smallint | 0=Draft 1=Sent 2=Acknowledged 3=Recalled |
| SentOn / AcknowledgedOn | datetime nullable | Populated at state transitions |
| TenantId | int | Indexes: (ToEmployeeId, TenantId, Status), (FromEmployeeId, TenantId, Status) |
TRECOGNITION — Recognition & Appreciation
| Column | Type | Notes |
|---|---|---|
| RecognitionId / FromEmployeeId / ToEmployeeId | int | |
| RecognitionType | smallint | 0=Appreciation 1=Kudos 2=Award 3=SpotBonus |
| Message | nvarchar(max) | Recognition message text |
| IsPublic | bit | true = visible in recognition wall; false = recipient only |
| LinkedContextType / LinkedContextId | smallint / int | 0=None, 1=Goal, 2=Appraisal. LinkedContextId=-1 if None. |
| CreatedBy / CreatedOn | Audit only. No ModifiedBy/ModifiedOn — recognition is immutable. | |
| TenantId | int | Indexes: (ToEmployeeId, TenantId), (TenantId, IsPublic, CreatedOn DESC) for wall query |
Feedback Lifecycle
Status=0
Status=1 InProgress
Status=2
Status=3
Memo Lifecycle (Most Complex)
Recognition Flow
INSERT, immutable
Yes → Public
Push to fbck:client:{clientId} group — entire tenant sees it on recognition wall
Also push to fbck:user:{toEmployeeId} — personal notification
No → Private
Push only to fbck:user:{toEmployeeId}
Not visible in GetRecognitionWall; only in GetRecognitionList for recipient
FeedbackSummaryDTO — PERM ESS Cross-Module
FBCK exposes a lean summary method called by PermESSBLL.GetMyPerformanceSummary to aggregate feedback counts into one ESS response without loading full feedback records:
// FBCKDAL/DTOs/FeedbackSummaryDTO.cs
public class FeedbackSummaryDTO {
public int FeedbackCount { get; set; } // total feedback received, last 90 days
public int PendingResponseCount { get; set; } // Requested/InProgress (Status=0 or 1)
public int UnacknowledgedMemos { get; set; } // TMEMO Status=1 for this EmployeeId
public int RecentRecognitionCount { get; set; } // TRECOGNITION last 30 days for this employee
}
// IFeedbackBLL interface method
Task<FeedbackSummaryDTO> GetFeedbackSummaryForESS(int employeeId, LoginDTO login, CancellationToken ct);
FeedbackHub — SignalR
Hub Configuration
| Property | Value |
|---|---|
| Hub class | FeedbackHub : Hub<IFBCKClient> |
| Route | /hubs/feedback |
| MaximumReceiveMessageSize | 32 KB |
| KeepAliveInterval | 15 seconds |
| ClientTimeoutInterval | 60 seconds |
| EnableDetailedErrors | Development only |
IFBCKClient — Strongly-Typed Client Contract
// FBCKSL/Hubs/IFBCKClient.cs
public interface IFBCKClient
{
Task ReceiveFeedbackNotification(FeedbackDTO feedback);
Task ReceiveMemoNotification(MemoDTO memo);
Task ReceiveRecognitionNotification(RecognitionDTO recognition);
Task ReceiveActionItemReminder(ActionItemDTO actionItem);
Task ReceiveError(string errorMessage);
}
Hub Group Strategy — Dual Membership
// FeedbackHub.cs
public override async Task OnConnectedAsync()
{
var login = GetLoginDTO();
// Personal notifications group
await Groups.AddToGroupAsync(Context.ConnectionId, $"fbck:user:{login.UserId}");
// Tenant-wide recognition wall feed
await Groups.AddToGroupAsync(Context.ConnectionId, $"fbck:client:{login.ClientId}");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var login = GetLoginDTO();
// CRITICAL: clean up BOTH groups on disconnect
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"fbck:user:{login.UserId}");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"fbck:client:{login.ClientId}");
await base.OnDisconnectedAsync(exception);
}
Server-Initiated Push (MemoBLL)
// MemoBLL.cs — injecting IHubContext for push after SendMemo
public class MemoBLL(
IMemoDAL dal,
IValidation validation,
IKeyInvalidate keyInvalidate,
IEventLogPublish eventLog,
ILogger<MemoBLL> logger,
IHubContext<FeedbackHub, IFBCKClient> hub) // ← inject for server-push
{
public async Task<string> SendMemoAsync(int memoId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("validate-memo-send", new { memoId });
var memo = await _dal.GetMemoAsync(memoId, login, ct);
if (memo.Status != 0) throw new ValidationException("Only Draft memos can be sent");
GB5Trace.Step("send-memo", new { memoId });
await _dal.SendMemoAsync(memoId, login, ct);
GB5Trace.Step("event-publish", new { EventTypes.FBCKMEMOSSENT });
await _eventLog.PublishEventLogAsync(new EventLogDTO { ... }, login);
// Push to recipient's personal group
await _hub.Clients.Group($"fbck:user:{memo.ToEmployeeId}").ReceiveMemoNotification(memo);
return SuccessResponse.UpdateSuccess;
}
}
Hub is Receive-Only
API Endpoints Reference
Feedback
| Method | Route | Description | Cache |
|---|---|---|---|
| GET | /Feedback/GetFeedback {FeedbackId} | Single feedback record | NOT_REQUIRED |
| GET | /Feedback/GetFeedbackList | Paged; TargetEmployeeId, ResponderId, Status | USER_LEVEL 2 min |
| POST | /Feedback/RequestFeedback | Create new feedback request | null |
| POST | /Feedback/SubmitFeedback {FeedbackId} | Responder submits; Status=2; SignalR push | null |
| POST | /Feedback/DeclineFeedback {FeedbackId} | Responder declines; Status=3; guard: not after Submitted | null |
One-on-One Notes
| Method | Route | Cache |
|---|---|---|
| GET | /Feedback/OneOnOne/GetOneOnOneList | USER_LEVEL 3 min |
| POST | /Feedback/OneOnOne/SaveOneOnOneNote | null — creates or updates (Draft) |
| POST | /Feedback/OneOnOne/CompleteOneOnOne {TONONoteId} | null — Status→1 |
Action Items
| Method | Route | Cache |
|---|---|---|
| GET | /Feedback/ActionItem/GetActionItems {EmployeeId?, ContextType?, ContextId?, Status?} | USER_LEVEL 2 min |
| POST | /Feedback/ActionItem/SaveActionItem | null |
| POST | /Feedback/ActionItem/CompleteActionItem {ActionItemId, CompletionRemarks} | null |
Memos
| Method | Route | Notes | Cache |
|---|---|---|---|
| GET | /Feedback/Memo/GetMemo {MemoId} | ContentHtml redacted if IsConfidential and Status=1 | NOT_REQUIRED |
| GET | /Feedback/Memo/GetMemoList | Paged; ToEmployeeId, MemoType, Status | USER_LEVEL 2 min |
| POST | /Feedback/Memo/SaveMemo | Create/update (Draft only) | null |
| POST | /Feedback/Memo/SendMemo {MemoId} | Status→1; SentOn; SignalR push to recipient | null |
| POST | /Feedback/Memo/AcknowledgeMemo {MemoId} | ESS — EmployeeId from login.UserId; Status→2 | null |
| POST | /Feedback/Memo/RecallMemo {MemoId} | HR only; Status→3; allowed from 1 or 2 | null |
Recognition
| Method | Route | Cache |
|---|---|---|
| GET | /Feedback/Recognition/GetRecognitionWall {DeptId?, OKRCycleId?} | NOT_REQUIRED — live public feed |
| GET | /Feedback/Recognition/GetRecognitionList {ToEmployeeId, page, pageSize} | USER_LEVEL 2 min |
| POST | /Feedback/Recognition/SaveRecognition | null — triggers SignalR push |
| DELETE | /Feedback/Recognition/DeleteRecognition {RecognitionId} | null |
Sample Scenarios
Scenario A — 360 Feedback Request
POST /Feedback/RequestFeedback
{
"TargetEmployeeId": 201,
"ResponderId": 305,
"FeedbackType": 4, // 360
"ContextType": 1, // AppraisalLinked
"ContextId": 1042, // AppraisalId
"QuestionsJson": "[\"How effectively does this person collaborate with cross-functional teams?\",\"Rate their communication skills (1-5)\",\"What is one area they should focus on improving?\"]",
"DueDate": "2026-04-15"
}
// Response: FeedbackId: 7823, Status: 0 (Requested)
// SignalR: push ReceiveFeedbackNotification to fbck:user:305 (the responder)
Scenario B — Disciplinary Memo Full Flow
// Step 1: HR creates confidential disciplinary memo (Draft)
POST /Feedback/Memo/SaveMemo
{ "ToEmployeeId": 201, "MemoType": 1, "Subject": "Attendance Warning - March 2026",
"ContentHtml": "<p>Dear John, this formal warning...</p>",
"IsConfidential": true }
// Result: MemoId: 312, Status: 0 (Draft)
// Step 2: HR sends memo
POST /Feedback/Memo/SendMemo { "MemoId": 312 }
// Result: Status → 1, SentOn: 2026-03-20 09:00
// SignalR push to fbck:user:201 → ReceiveMemoNotification
// ESS shows: "1 unacknowledged memo" but ContentHtml is HIDDEN (IsConfidential=true, Status=1)
// Step 3: Employee acknowledges (ESS)
POST /Feedback/Memo/AcknowledgeMemo { "MemoId": 312 }
// EmployeeId taken from login.UserId (not from request body)
// Result: Status → 2, AcknowledgedOn: 2026-03-21 14:30
// ContentHtml now revealed in ESS
Scenario C — Recognition Wall
Scenario D — 1:1 Meeting with Action Items
// SaveOneOnOneNote
{
"EmployeeId": 201, "ManagerId": 150, "ScheduledDate": "2026-03-22",
"AgendaJson": "[\"Q1 OKR review — goal at 75%\",\"Career development discussion\",\"Team collaboration concerns\"]",
"NotesJson": "[\"ARR goal strong — 22/30 leads qualified\",\"Interested in Tech Lead path by Q3\",\"Friction with design team on specs\"]",
"Status": 0
}
// Action items created from meeting:
POST /Feedback/ActionItem/SaveActionItem
{ "ContextType": 0, "ContextId": 891, // TONONoteId
"EmployeeId": 201, "OwnerId": 150,
"ActionText": "Schedule design team retrospective with Lisa",
"DueDate": "2026-04-01" }
POST /Feedback/ActionItem/SaveActionItem
{ "ContextType": 0, "ContextId": 891,
"EmployeeId": 150, "OwnerId": 150,
"ActionText": "Send Tech Lead career path resources and book 1:1 with L&D",
"DueDate": "2026-03-28" }
Integration Map
| Direction | From → To | Mechanism | What |
|---|---|---|---|
| FBCK → PERM | FBCK → PERM | Direct BLL injection (IFeedbackBLL) | GetFeedbackSummaryForESS called by PermESSBLL.GetMyPerformanceSummary |
| FBCK → ESS Push | MemoBLL / RecognitionBLL → SignalR | IHubContext injection | Memo sent, recognition given → push to user group |
| FBCK → Dapr | FeedbackBLL → Dapr pubsub | IEventLogPublish | fbck.feedback.requested, fbck.feedback.submitted, fbck.1on1.scheduled, fbck.memo.sent, fbck.recognition.given |
Service Configuration
| Setting | Value |
|---|---|
| Port | 5124 |
| Proxy prefix | /fbck/ |
| SignalR hub route | /hubs/feedback |
| Assembly scanning | FBCKBLL + FBCKDAL (in FBCKSL/Program.cs) |
| PERMBLL reference | ProjectReference to FBCKBLL.csproj (for ESS aggregation) |
| Cross-module refs | FBCKBLL.csproj has NO cross-module references — standalone |
Caching Strategy
| Data | Level | TTL | Invalidation |
|---|---|---|---|
| Feedback list per user | USER_LEVEL | 2 min | RequestFeedback, SubmitFeedback, DeclineFeedback |
| 1:1 notes list | USER_LEVEL | 3 min | Save, Complete |
| Action items list | USER_LEVEL | 2 min | Save, Complete |
| Memo list per user | USER_LEVEL | 2 min | Send, Acknowledge, Recall |
| Single feedback/memo/note | NOT_REQUIRED | — | Volatile — always live |
| Recognition wall | NOT_REQUIRED | — | Live feed; public visibility changes immediately |
| Recognition list per user | USER_LEVEL | 2 min | SaveRecognition, DeleteRecognition |
ESS Employee View
What an employee sees in their FBCK-sourced dashboard sections (via PERM ESS aggregation):
| Section | Data | Source |
|---|---|---|
| Pending Feedback | Feedback requests I must respond to (Status=0 or 1) | TFEEDBACK ResponderId=me |
| Feedback Received | Completed feedback about me (last 90 days) | TFEEDBACK TargetEmployeeId=me |
| My Memos | All memos sent to me (confidential redacted until ack) | TMEMO ToEmployeeId=me |
| Unacknowledged Memos | Count badge + alert | TMEMO Status=1 ToEmployeeId=me |
| Recognitions Received | Timeline of kudos/awards/appreciations | TRECOGNITION ToEmployeeId=me |
| My Action Items | Outstanding + overdue items by DueDate | TACTIONITEM EmployeeId=me |