FBCK Continuous Feedback Module

Continuous Feedback (FBCK)

Peer feedback · 1:1 meeting notes · formal HR memos · recognition wall · real-time SignalR push — unified continuous feedback culture

5
Database Tables
23
API Endpoints
2
SignalR Groups/User
5
Dapr Events Published

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

📝 Feedback

Peer, manager, upward, self, and 360 feedback requests. Configurable questions JSON. Can be linked to an appraisal or goal context.

🤝 One-on-One Notes

Structured 1:1 meeting notes with agenda, notes, and action item creation. Dual-side: employee and manager each contribute.

📋 Memos

Formal HR/manager communications: commendation, disciplinary, informational, policy. Confidential memos hidden until acknowledged.

🏆 Recognition

Appreciation, kudos, awards, spot bonuses. Public recognitions visible on tenant-wide recognition wall. Immutable once created.

Key Concepts Glossary

TermDefinition
FeedbackType0=Peer, 1=Manager, 2=Self, 3=Upward, 4=360. Determines who is giving feedback about whom.
ContextType0=Standalone (independent feedback), 1=AppraisalLinked (tied to a TAPPRAISAL), 2=GoalLinked (tied to a TGOAL).
ContextIdThe AppraisalId or GoalId when ContextType > 0. Stored as -1 when Standalone.
QuestionsJson / ResponsesJsonFlexible JSON arrays for feedback questions and answers. Allows custom question sets per feedback request without schema changes.
MemoType0=Commendation, 1=Disciplinary, 2=Informational, 3=PolicyUpdate. Determines icon, color, and HR access level.
IsConfidentialBit 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.
RecognitionType0=Appreciation, 1=Kudos, 2=Award, 3=SpotBonus. Drives display style and notification urgency.
IsPublicBit on TRECOGNITION. Public recognitions appear in the tenant-wide recognition wall feed; private ones visible only to recipient.
FeedbackSummaryDTOLean 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 MembershipEvery connected user joins BOTH groups. Group cleanup must happen in OnDisconnectedAsync for both.

Database Schema

TFEEDBACK — Feedback Requests

ColumnTypeNotes
FeedbackIdint PK
RequestedById / TargetEmployeeId / ResponderIdintWho requested, subject, who responds
FeedbackTypesmallint0=Peer 1=Manager 2=Self 3=Upward 4=360
ContextType / ContextIdsmallint / int0=Standalone; 1=AppraisalLinked; 2=GoalLinked. ContextId=-1 when Standalone.
QuestionsJson / ResponsesJsonnvarchar(max)JSON arrays — flexible question sets per request
OverallRatingdecimal(5,2)0 = not rated
Statussmallint0=Requested 1=InProgress 2=Submitted 3=Declined
DueDatedate nullable
TenantIdintIndexes: (TargetEmployeeId, TenantId, Status), (ResponderId, TenantId, Status)

TONONOTE — 1:1 Meeting Notes

ColumnTypeNotes
TONONoteId / EmployeeId / ManagerIdint
ScheduledDate / ActualDatetimedate / datetimeActualDatetime set when meeting actually held
AgendaJsonnvarchar(max)JSON array of agenda topics pre-meeting
NotesJsonnvarchar(max)JSON notes captured during/after meeting
Statussmallint0=Scheduled, 1=Completed, 2=Cancelled
TenantIdintIndexes: (EmployeeId, TenantId, Status), (ManagerId, TenantId, ScheduledDate)

TACTIONITEM — Action Items

ColumnTypeNotes
ActionItemIdint PK
ContextType / ContextIdsmallint / int0=OneOnOne(TONONoteId), 1=Feedback(FeedbackId), 2=Standalone(-1)
EmployeeId / OwnerIdintWho must complete / who created it
ActionText / DueDatenvarchar / date
Statussmallint0=Pending 1=Completed 2=Overdue
CompletionRemarksnvarchar nullableRequired on completion
TenantIdintIndexes: (EmployeeId, TenantId, Status), (DueDate, TenantId, Status)

TMEMO — Formal HR/Manager Memos

ColumnTypeNotes
MemoId / FromEmployeeId / ToEmployeeIdint
MemoTypesmallint0=Commendation 1=Disciplinary 2=Informational 3=PolicyUpdate
Subject / ContentHtmlnvarcharRich HTML content
IsConfidentialbitWhen true: hidden from ESS until Status=2
Statussmallint0=Draft 1=Sent 2=Acknowledged 3=Recalled
SentOn / AcknowledgedOndatetime nullablePopulated at state transitions
TenantIdintIndexes: (ToEmployeeId, TenantId, Status), (FromEmployeeId, TenantId, Status)

TRECOGNITION — Recognition & Appreciation

ColumnTypeNotes
RecognitionId / FromEmployeeId / ToEmployeeIdint
RecognitionTypesmallint0=Appreciation 1=Kudos 2=Award 3=SpotBonus
Messagenvarchar(max)Recognition message text
IsPublicbittrue = visible in recognition wall; false = recipient only
LinkedContextType / LinkedContextIdsmallint / int0=None, 1=Goal, 2=Appraisal. LinkedContextId=-1 if None.
CreatedBy / CreatedOnAudit only. No ModifiedBy/ModifiedOn — recognition is immutable.
TenantIdintIndexes: (ToEmployeeId, TenantId), (TenantId, IsPublic, CreatedOn DESC) for wall query
Immutability: TRECOGNITION is INSERT + DELETE only. No UPDATE. A recognition once given cannot be edited — only deleted by the original creator or HR. This preserves trust in the recognition record.

Feedback Lifecycle

Request Sent
Status=0
→
Responder Views
Status=1 InProgress
→
Submit Response
Status=2
— OR —
Status=0 or 1
→
Declined
Status=3
Guard: Cannot submit or decline a feedback that is already Submitted (Status=2). Cannot re-submit or change a Declined feedback. These guards are enforced in FeedbackBLL with ValidationException.

Memo Lifecycle (Most Complex)

0 Draft
→ HR/Manager creates; editable; not visible to recipient
1 Sent
→ SendMemo → SentOn populated; SignalR push to fbck:user:{recipientId}; no longer editable
2 Acknowledged
→ Employee ESS: AcknowledgeMemo → AcknowledgedOn populated; confidential content revealed
↓ HR can still recall
3 Recalled
→ RecallMemo; allowed from Status=1 or 2; HR decision
Confidential memos: When IsConfidential=true and Status=1 (Sent), the employee's ESS GetMemoList shows the memo exists (subject visible) but ContentHtml is redacted. Once Status=2 (Acknowledged), full content is revealed. This ensures disciplinary notices are formally delivered before the employee can claim ignorance.

Recognition Flow

SaveRecognition
INSERT, immutable
→
IsPublic = true?

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);
PERMBLL.csproj references FBCKBLL.csproj via ProjectReference. FBCKSL runs as an independent microservice on port 5124 — PERMBLL does NOT call FBCK via HTTP; it's a direct in-process assembly reference when deployed together, or PERM uses its own FBCK BLL assembly reference.

FeedbackHub — SignalR

Hub Configuration

PropertyValue
Hub classFeedbackHub : Hub<IFBCKClient>
Route/hubs/feedback
MaximumReceiveMessageSize32 KB
KeepAliveInterval15 seconds
ClientTimeoutInterval60 seconds
EnableDetailedErrorsDevelopment 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);
}
Memory Leak Prevention: Failing to remove from BOTH groups in OnDisconnectedAsync causes groups to accumulate stale connection IDs indefinitely. This is a SignalR-specific memory leak that grows proportionally to user churn.

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

FeedbackHub has no Hub methods (no SendFeedback, no SendMemo Hub method). All writes go through REST FastEndpoints. The hub only sends data TO clients — it never receives business operations FROM clients. This eliminates the risk of auth bypass through WebSocket channel.

API Endpoints Reference

Feedback

MethodRouteDescriptionCache
GET/Feedback/GetFeedback {FeedbackId}Single feedback recordNOT_REQUIRED
GET/Feedback/GetFeedbackListPaged; TargetEmployeeId, ResponderId, StatusUSER_LEVEL 2 min
POST/Feedback/RequestFeedbackCreate new feedback requestnull
POST/Feedback/SubmitFeedback {FeedbackId}Responder submits; Status=2; SignalR pushnull
POST/Feedback/DeclineFeedback {FeedbackId}Responder declines; Status=3; guard: not after Submittednull

One-on-One Notes

MethodRouteCache
GET/Feedback/OneOnOne/GetOneOnOneListUSER_LEVEL 3 min
POST/Feedback/OneOnOne/SaveOneOnOneNotenull — creates or updates (Draft)
POST/Feedback/OneOnOne/CompleteOneOnOne {TONONoteId}null — Status→1

Action Items

MethodRouteCache
GET/Feedback/ActionItem/GetActionItems {EmployeeId?, ContextType?, ContextId?, Status?}USER_LEVEL 2 min
POST/Feedback/ActionItem/SaveActionItemnull
POST/Feedback/ActionItem/CompleteActionItem {ActionItemId, CompletionRemarks}null

Memos

MethodRouteNotesCache
GET/Feedback/Memo/GetMemo {MemoId}ContentHtml redacted if IsConfidential and Status=1NOT_REQUIRED
GET/Feedback/Memo/GetMemoListPaged; ToEmployeeId, MemoType, StatusUSER_LEVEL 2 min
POST/Feedback/Memo/SaveMemoCreate/update (Draft only)null
POST/Feedback/Memo/SendMemo {MemoId}Status→1; SentOn; SignalR push to recipientnull
POST/Feedback/Memo/AcknowledgeMemo {MemoId}ESS — EmployeeId from login.UserId; Status→2null
POST/Feedback/Memo/RecallMemo {MemoId}HR only; Status→3; allowed from 1 or 2null

Recognition

MethodRouteCache
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/SaveRecognitionnull — 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

Sarah Chen → Alex Kumar
🏆 Award
"Alex's work on the Q1 enterprise pipeline was exceptional — 22 qualified leads, well above target. The partnership he built with the solutions team made all the difference."
Mike Ross → Priya Sharma
👏 Kudos
"Priya jumped in and fixed the critical production issue at midnight without being asked. True ownership mindset."
HR Team → David Lee
💰 Spot Bonus
"Recognizing David's outstanding contribution to the compliance audit. His preparation saved the company significant regulatory risk."

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

DirectionFrom → ToMechanismWhat
FBCK → PERMFBCK → PERMDirect BLL injection (IFeedbackBLL)GetFeedbackSummaryForESS called by PermESSBLL.GetMyPerformanceSummary
FBCK → ESS PushMemoBLL / RecognitionBLL → SignalRIHubContext injectionMemo sent, recognition given → push to user group
FBCK → DaprFeedbackBLL → Dapr pubsubIEventLogPublishfbck.feedback.requested, fbck.feedback.submitted, fbck.1on1.scheduled, fbck.memo.sent, fbck.recognition.given

Service Configuration

SettingValue
Port5124
Proxy prefix/fbck/
SignalR hub route/hubs/feedback
Assembly scanningFBCKBLL + FBCKDAL (in FBCKSL/Program.cs)
PERMBLL referenceProjectReference to FBCKBLL.csproj (for ESS aggregation)
Cross-module refsFBCKBLL.csproj has NO cross-module references — standalone

Caching Strategy

DataLevelTTLInvalidation
Feedback list per userUSER_LEVEL2 minRequestFeedback, SubmitFeedback, DeclineFeedback
1:1 notes listUSER_LEVEL3 minSave, Complete
Action items listUSER_LEVEL2 minSave, Complete
Memo list per userUSER_LEVEL2 minSend, Acknowledge, Recall
Single feedback/memo/noteNOT_REQUIRED—Volatile — always live
Recognition wallNOT_REQUIRED—Live feed; public visibility changes immediately
Recognition list per userUSER_LEVEL2 minSaveRecognition, DeleteRecognition

ESS Employee View

What an employee sees in their FBCK-sourced dashboard sections (via PERM ESS aggregation):

SectionDataSource
Pending FeedbackFeedback requests I must respond to (Status=0 or 1)TFEEDBACK ResponderId=me
Feedback ReceivedCompleted feedback about me (last 90 days)TFEEDBACK TargetEmployeeId=me
My MemosAll memos sent to me (confidential redacted until ack)TMEMO ToEmployeeId=me
Unacknowledged MemosCount badge + alertTMEMO Status=1 ToEmployeeId=me
Recognitions ReceivedTimeline of kudos/awards/appreciationsTRECOGNITION ToEmployeeId=me
My Action ItemsOutstanding + overdue items by DueDateTACTIONITEM EmployeeId=me
Security: All ESS calls use EmployeeId = login.UserId server-side. Passing a different EmployeeId in request parameters is ignored. AcknowledgeMemo extracts the acknowledging EmployeeId from the token only — employees cannot acknowledge memos on behalf of others.