feat(workforce): add role-based activity weights
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
## Назначение
|
||||
|
||||
Продукт предназначен для централизованного контроля состояния рабочих мест,
|
||||
оценки полезной активности сотрудников, анализа загрузки подразделений,
|
||||
оценки активности сотрудников, анализа загрузки подразделений,
|
||||
контроля серверных сервисов, ActivityWatch telemetry, рабочих интервалов,
|
||||
операторских runbook-проверок и прикладных событий расследования.
|
||||
|
||||
## Коммерческие модули
|
||||
|
||||
- `DetMir Workforce` - ежедневный управленческий слой: полезная активность,
|
||||
- `DetMir Workforce` - ежедневный управленческий слой: активность,
|
||||
активное время, простои, RDP/1C/рабочие приложения, загрузка сотрудников и
|
||||
отчеты для руководителя.
|
||||
- `DetMir Security` - прикладной слой ИБ: DLP-сигналы, evidence metadata,
|
||||
|
||||
@@ -11,7 +11,7 @@ evidence и Hayabusa используются как прикладные мод
|
||||
|
||||
## Назначение
|
||||
|
||||
- DetMir Workforce: полезная активность, загрузка сотрудников, RDP/1C/рабочие
|
||||
- DetMir Workforce: активность сотрудников, загрузка, RDP/1C/рабочие
|
||||
приложения и управленческие отчеты для владельца бизнеса.
|
||||
- DetMir Security: DLP-сигналы, evidence, очередь кейсов и audit действий
|
||||
оператора без заявления продукта как сертифицированной СЗИ.
|
||||
@@ -42,7 +42,7 @@ runtime, OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти
|
||||
|
||||
## Кому это полезно
|
||||
|
||||
- Владельцу и руководителю - видеть полезную активность, загрузку команды,
|
||||
- Владельцу и руководителю - видеть активность, загрузку команды,
|
||||
простои, перегрузки и рабочие приложения без просмотра логов.
|
||||
- ИБ - заметить DLP-сигналы и подозрительную активность.
|
||||
- Администратору - проверить, что сборщики и сервер работают стабильно.
|
||||
|
||||
@@ -64,6 +64,13 @@ struct Cli {
|
||||
)]
|
||||
one_c_url: String,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "/etc/detmir-portal-workforce-policy.json",
|
||||
env = "DETMIR_PORTAL_WORKFORCE_POLICY_PATH"
|
||||
)]
|
||||
workforce_policy_path: PathBuf,
|
||||
|
||||
#[arg(long, default_value_t = 10, env = "DETMIR_PORTAL_TIMEOUT_SECONDS")]
|
||||
timeout_seconds: u64,
|
||||
|
||||
@@ -372,6 +379,37 @@ struct ReportMetrics {
|
||||
workforce_index: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct WorkforcePolicy {
|
||||
#[serde(default = "default_workforce_role")]
|
||||
default_role: String,
|
||||
#[serde(default)]
|
||||
roles: BTreeMap<String, WorkforceRolePolicy>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
struct WorkforceRolePolicy {
|
||||
#[serde(default)]
|
||||
label: Option<String>,
|
||||
#[serde(default)]
|
||||
planned_hours_per_day: Option<f64>,
|
||||
#[serde(default)]
|
||||
default_weight: Option<f64>,
|
||||
#[serde(default)]
|
||||
application_weights: BTreeMap<String, f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WeightedActivity {
|
||||
role: String,
|
||||
role_label: String,
|
||||
index: Option<u8>,
|
||||
planned_seconds: i64,
|
||||
app_seconds: i64,
|
||||
weighted_seconds: i64,
|
||||
matched_applications: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let code = match run() {
|
||||
Ok(code) => code,
|
||||
@@ -391,7 +429,7 @@ fn run() -> Result<i32> {
|
||||
let smoke = json!({
|
||||
"health": build_health(&snapshot),
|
||||
"summary": build_summary(&snapshot),
|
||||
"reports": build_reports(&snapshot, &incident_state, &build_dlp_evidence_response(&args)),
|
||||
"reports": build_reports(&snapshot, &incident_state, &build_dlp_evidence_response(&args), &args.workforce_policy_path),
|
||||
"incidents": build_incidents(&snapshot, &incident_state),
|
||||
"dlp_evidence": build_dlp_evidence_response(&args),
|
||||
});
|
||||
@@ -465,7 +503,12 @@ fn handle_request(request: Request, args: &Cli) -> Result<()> {
|
||||
let evidence = build_dlp_evidence_response(args);
|
||||
respond_json(
|
||||
request,
|
||||
&build_reports(&snapshot, &incident_state, &evidence),
|
||||
&build_reports(
|
||||
&snapshot,
|
||||
&incident_state,
|
||||
&evidence,
|
||||
&args.workforce_policy_path,
|
||||
),
|
||||
)
|
||||
}
|
||||
"/api/incidents" => {
|
||||
@@ -841,6 +884,7 @@ fn build_reports(
|
||||
snapshot: &Snapshot,
|
||||
incident_state: &IncidentStateFile,
|
||||
evidence: &DlpEvidenceResponse,
|
||||
workforce_policy_path: &Path,
|
||||
) -> Value {
|
||||
let summary = build_summary(snapshot);
|
||||
let incidents = build_incidents(snapshot, incident_state);
|
||||
@@ -868,6 +912,10 @@ fn build_reports(
|
||||
let worktime = worktime_block(snapshot);
|
||||
let one_c = one_c_block(snapshot);
|
||||
let dlp_block_value = dlp_block(snapshot);
|
||||
let weighted = load_workforce_policy(workforce_policy_path)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|policy| weighted_activity(snapshot, &policy, metrics.users_count));
|
||||
let headline = if summary.operator_ok && summary.severity == "OK" && metrics.open_incidents == 0
|
||||
{
|
||||
"Контур DetMir работает штатно, критичных действий не требуется"
|
||||
@@ -906,7 +954,8 @@ fn build_reports(
|
||||
"headline": headline,
|
||||
"executive_points": executive_points,
|
||||
"kpis": [
|
||||
report_kpi("Индекс активности", workforce_index_text(metrics.workforce_index), workforce_index_status(metrics.workforce_index), "proxy: active time / 8h на сотрудника"),
|
||||
report_kpi("Индекс активности", workforce_index_text(metrics.workforce_index), workforce_index_status(metrics.workforce_index), "proxy: активное время / плановое рабочее время"),
|
||||
weighted_activity_kpi(weighted.as_ref()),
|
||||
report_kpi("Сотрудники", metrics.users_count.to_string(), worktime.status.clone(), "строки worktime за сегодня"),
|
||||
report_kpi("Активное время", human_duration(metrics.active_seconds), worktime.status.clone(), "сумма active_seconds"),
|
||||
report_kpi("Приложения", metrics.apps_count.to_string(), worktime.status.clone(), "true active applications"),
|
||||
@@ -928,6 +977,7 @@ fn build_reports(
|
||||
"title": "Работа и управляемость",
|
||||
"items": [
|
||||
report_item("Индекс активности", workforce_index_status(metrics.workforce_index), workforce_index_text(metrics.workforce_index)),
|
||||
weighted_activity_item(weighted.as_ref(), workforce_policy_path),
|
||||
report_item("Worktime", worktime.status.clone(), worktime.text.clone()),
|
||||
report_item("Активное время", worktime.status.clone(), human_duration(metrics.active_seconds)),
|
||||
report_item("Приложения", worktime.status.clone(), metrics.apps_count.to_string()),
|
||||
@@ -948,11 +998,165 @@ fn build_reports(
|
||||
"items": recommendations.iter().map(|item| report_item("Рекомендация", "INFO", item)).collect::<Vec<_>>()
|
||||
}
|
||||
],
|
||||
"workforce_policy": workforce_policy_json(weighted.as_ref(), workforce_policy_path),
|
||||
"markdown": markdown,
|
||||
"links": links()
|
||||
})
|
||||
}
|
||||
|
||||
fn load_workforce_policy(path: &Path) -> Result<Option<WorkforcePolicy>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
|
||||
let policy = serde_json::from_str::<WorkforcePolicy>(&data)
|
||||
.with_context(|| format!("parse {}", path.display()))?;
|
||||
Ok(Some(policy))
|
||||
}
|
||||
|
||||
fn weighted_activity(
|
||||
snapshot: &Snapshot,
|
||||
policy: &WorkforcePolicy,
|
||||
users_count: usize,
|
||||
) -> Option<WeightedActivity> {
|
||||
if users_count == 0 {
|
||||
return None;
|
||||
}
|
||||
let role = policy.default_role.trim();
|
||||
let role_policy = policy.roles.get(role)?;
|
||||
let planned_hours = role_policy
|
||||
.planned_hours_per_day
|
||||
.unwrap_or(8.0)
|
||||
.clamp(1.0, 24.0);
|
||||
let planned_seconds = (users_count as f64 * planned_hours * 3600.0).round() as i64;
|
||||
if planned_seconds <= 0 {
|
||||
return None;
|
||||
}
|
||||
let default_weight = role_policy.default_weight.unwrap_or(0.0).clamp(0.0, 1.0);
|
||||
let apps = snapshot
|
||||
.worktime
|
||||
.payload
|
||||
.as_ref()
|
||||
.and_then(|payload| payload.get("true_active_apps"))
|
||||
.and_then(Value::as_array)?;
|
||||
let mut app_seconds = 0_i64;
|
||||
let mut weighted_seconds = 0_f64;
|
||||
let mut matched_applications = 0_usize;
|
||||
for app in apps {
|
||||
let name = app.get("application").and_then(Value::as_str).unwrap_or("");
|
||||
let seconds = app
|
||||
.get("proved_work_seconds")
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0)
|
||||
.max(0);
|
||||
if seconds == 0 {
|
||||
continue;
|
||||
}
|
||||
app_seconds += seconds;
|
||||
let weight = application_weight(role_policy, name, default_weight);
|
||||
if weight > 0.0 {
|
||||
matched_applications += 1;
|
||||
}
|
||||
weighted_seconds += seconds as f64 * weight;
|
||||
}
|
||||
let weighted_seconds_i64 = weighted_seconds.round() as i64;
|
||||
Some(WeightedActivity {
|
||||
role: role.to_string(),
|
||||
role_label: role_policy
|
||||
.label
|
||||
.clone()
|
||||
.unwrap_or_else(|| role.to_string()),
|
||||
index: Some(
|
||||
((weighted_seconds / planned_seconds as f64) * 100.0)
|
||||
.round()
|
||||
.clamp(0.0, 100.0) as u8,
|
||||
),
|
||||
planned_seconds,
|
||||
app_seconds,
|
||||
weighted_seconds: weighted_seconds_i64,
|
||||
matched_applications,
|
||||
})
|
||||
}
|
||||
|
||||
fn application_weight(
|
||||
role_policy: &WorkforceRolePolicy,
|
||||
application: &str,
|
||||
default_weight: f64,
|
||||
) -> f64 {
|
||||
let app = application.to_lowercase();
|
||||
role_policy
|
||||
.application_weights
|
||||
.iter()
|
||||
.find_map(|(pattern, weight)| {
|
||||
let pattern = pattern.to_lowercase();
|
||||
(!pattern.is_empty() && app.contains(&pattern)).then_some(weight.clamp(0.0, 1.0))
|
||||
})
|
||||
.unwrap_or(default_weight)
|
||||
}
|
||||
|
||||
fn weighted_activity_kpi(weighted: Option<&WeightedActivity>) -> Value {
|
||||
match weighted {
|
||||
Some(weighted) => report_kpi(
|
||||
"Взвешенная активность",
|
||||
workforce_index_text(weighted.index),
|
||||
workforce_index_status(weighted.index),
|
||||
&format!("role={} по весам приложений", weighted.role_label),
|
||||
),
|
||||
None => report_kpi(
|
||||
"Взвешенная активность",
|
||||
"не настроена".to_string(),
|
||||
"UNKNOWN".to_string(),
|
||||
"нужен workforce policy с весами приложений",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn weighted_activity_item(weighted: Option<&WeightedActivity>, policy_path: &Path) -> Value {
|
||||
match weighted {
|
||||
Some(weighted) => report_item(
|
||||
"Взвешенная активность",
|
||||
workforce_index_status(weighted.index),
|
||||
format!(
|
||||
"{}; роль {}; weighted {}; apps {}",
|
||||
workforce_index_text(weighted.index),
|
||||
weighted.role_label,
|
||||
human_duration(weighted.weighted_seconds),
|
||||
weighted.matched_applications
|
||||
),
|
||||
),
|
||||
None => report_item(
|
||||
"Взвешенная активность",
|
||||
"UNKNOWN",
|
||||
format!("policy не настроена: {}", policy_path.display()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn workforce_policy_json(weighted: Option<&WeightedActivity>, policy_path: &Path) -> Value {
|
||||
match weighted {
|
||||
Some(weighted) => json!({
|
||||
"configured": true,
|
||||
"path": policy_path.display().to_string(),
|
||||
"role": weighted.role,
|
||||
"role_label": weighted.role_label,
|
||||
"index": weighted.index,
|
||||
"planned_seconds": weighted.planned_seconds,
|
||||
"app_seconds": weighted.app_seconds,
|
||||
"weighted_seconds": weighted.weighted_seconds,
|
||||
"matched_applications": weighted.matched_applications,
|
||||
}),
|
||||
None => json!({
|
||||
"configured": false,
|
||||
"path": policy_path.display().to_string(),
|
||||
"note": "weighted activity requires role/application policy",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_workforce_role() -> String {
|
||||
"default".to_string()
|
||||
}
|
||||
|
||||
fn report_kpi(label: &str, value: String, status: String, context: &str) -> Value {
|
||||
json!({
|
||||
"label": label,
|
||||
@@ -992,7 +1196,7 @@ fn render_report_markdown(
|
||||
}
|
||||
));
|
||||
text.push_str(&format!(
|
||||
"- Индекс полезной активности: {}\n",
|
||||
"- Индекс активности: {}\n",
|
||||
workforce_index_text(metrics.workforce_index)
|
||||
));
|
||||
text.push_str(&format!(
|
||||
@@ -2566,6 +2770,7 @@ mod tests {
|
||||
failed_units_cmd: "true".to_string(),
|
||||
worktime_url: "http://127.0.0.1".to_string(),
|
||||
one_c_url: "http://127.0.0.1".to_string(),
|
||||
workforce_policy_path: dir.path().join("workforce-policy.json"),
|
||||
timeout_seconds: 1,
|
||||
state_dir: dir.path().join("state"),
|
||||
dlp_db_path: dir.path().join("dlp.sqlite"),
|
||||
@@ -2686,7 +2891,8 @@ mod tests {
|
||||
{"user": "USER-2", "active_seconds": 1800}
|
||||
],
|
||||
"true_active_apps": [
|
||||
{"application": "ERP", "proved_work_human": "00:30"}
|
||||
{"application": "ERP", "proved_work_human": "00:30", "proved_work_seconds": 3600},
|
||||
{"application": "Browser", "proved_work_human": "00:30", "proved_work_seconds": 3600}
|
||||
]
|
||||
})),
|
||||
},
|
||||
@@ -2731,7 +2937,13 @@ mod tests {
|
||||
}],
|
||||
error: None,
|
||||
};
|
||||
let report = build_reports(&snapshot, &IncidentStateFile::default(), &evidence);
|
||||
let missing_policy = Path::new("/tmp/detmir-missing-workforce-policy.json");
|
||||
let report = build_reports(
|
||||
&snapshot,
|
||||
&IncidentStateFile::default(),
|
||||
&evidence,
|
||||
missing_policy,
|
||||
);
|
||||
assert_eq!(report["operator_ok"], true);
|
||||
assert_eq!(report["severity"], "OK");
|
||||
assert!(
|
||||
@@ -2741,5 +2953,73 @@ mod tests {
|
||||
.contains("derived detections/cases")
|
||||
);
|
||||
assert!(report["kpis"].as_array().unwrap().len() >= 6);
|
||||
assert_eq!(report["workforce_policy"]["configured"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weighted_activity_uses_role_application_policy() {
|
||||
let snapshot = Snapshot {
|
||||
generated_at_utc: "2026-06-03T10:00:00Z".to_string(),
|
||||
detmir_status: SourceStatus {
|
||||
ok: true,
|
||||
status: "OK".to_string(),
|
||||
summary: "".to_string(),
|
||||
error: None,
|
||||
payload: Some(json!({})),
|
||||
},
|
||||
detmir_check: SourceStatus {
|
||||
ok: true,
|
||||
status: "OK".to_string(),
|
||||
summary: "".to_string(),
|
||||
error: None,
|
||||
payload: Some(json!({})),
|
||||
},
|
||||
failed_units: SourceStatus {
|
||||
ok: true,
|
||||
status: "OK".to_string(),
|
||||
summary: "".to_string(),
|
||||
error: None,
|
||||
payload: None,
|
||||
},
|
||||
worktime: SourceStatus {
|
||||
ok: true,
|
||||
status: "OK".to_string(),
|
||||
summary: "".to_string(),
|
||||
error: None,
|
||||
payload: Some(json!({
|
||||
"true_active_apps": [
|
||||
{"application": "1С", "proved_work_seconds": 3600},
|
||||
{"application": "YouTube", "proved_work_seconds": 3600}
|
||||
]
|
||||
})),
|
||||
},
|
||||
one_c: SourceStatus {
|
||||
ok: true,
|
||||
status: "OK".to_string(),
|
||||
summary: "".to_string(),
|
||||
error: None,
|
||||
payload: None,
|
||||
},
|
||||
};
|
||||
let policy = WorkforcePolicy {
|
||||
default_role: "accountant".to_string(),
|
||||
roles: BTreeMap::from([(
|
||||
"accountant".to_string(),
|
||||
WorkforceRolePolicy {
|
||||
label: Some("Бухгалтер".to_string()),
|
||||
planned_hours_per_day: Some(8.0),
|
||||
default_weight: Some(0.2),
|
||||
application_weights: BTreeMap::from([
|
||||
("1с".to_string(), 1.0),
|
||||
("youtube".to_string(), 0.0),
|
||||
]),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
let weighted = weighted_activity(&snapshot, &policy, 1).unwrap();
|
||||
assert_eq!(weighted.role, "accountant");
|
||||
assert_eq!(weighted.weighted_seconds, 3600);
|
||||
assert_eq!(weighted.app_seconds, 7200);
|
||||
assert_eq!(weighted.index, Some(13));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ function renderManager(data) {
|
||||
<section class="card">
|
||||
<h3>Работа сегодня</h3>
|
||||
<p class="kpi-value">${escapeHtml(workforceIndex)}</p>
|
||||
<p class="muted">Индекс полезной активности: active time / 8 ч на сотрудника</p>
|
||||
<p class="muted">proxy: активное время / плановое рабочее время</p>
|
||||
<p class="muted">Сотрудников: ${data.users_count}; активных часов: ${Number(data.total_active_hours || 0).toFixed(1)}</p>
|
||||
<p class="muted">${escapeHtml(data.status?.text || "")}</p>
|
||||
</section>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
detmir_portal_worktime_url: "{{ detmir_portal_worktime_url_override | default(aw_worktime_report_base | default('http://192.0.2.13:5610', true), true) }}"
|
||||
detmir_portal_one_c_host: "{{ hostvars[(groups['proxmox'] | default([]) | first) | default('192.0.2.2', true)].ansible_host | default((groups['proxmox'] | default([]) | first) | default('192.0.2.2', true), true) }}"
|
||||
detmir_portal_one_c_url: "{{ detmir_portal_one_c_url_override | default('http://' + detmir_portal_one_c_host + ':8710', true) }}"
|
||||
detmir_portal_workforce_policy_path: "/etc/detmir-portal-workforce-policy.json"
|
||||
|
||||
tasks:
|
||||
- name: Check local detmir-portal binary
|
||||
@@ -47,6 +48,7 @@
|
||||
DETMIR_PORTAL_FAILED_UNITS_CMD=systemctl --failed --no-pager
|
||||
DETMIR_PORTAL_WORKTIME_URL={{ detmir_portal_worktime_url }}
|
||||
DETMIR_PORTAL_ONE_C_URL={{ detmir_portal_one_c_url }}
|
||||
DETMIR_PORTAL_WORKFORCE_POLICY_PATH={{ detmir_portal_workforce_policy_path }}
|
||||
DETMIR_PORTAL_TIMEOUT_SECONDS=10
|
||||
DETMIR_PORTAL_STATE_DIR=/var/lib/detmir-portal
|
||||
DETMIR_PORTAL_DLP_DB_PATH=/var/lib/activitywatch/dlp_warehouse.sqlite
|
||||
@@ -54,6 +56,15 @@
|
||||
DETMIR_PORTAL_EVIDENCE_LIMIT=30
|
||||
DETMIR_PORTAL_EVIDENCE_MAX_BYTES=8388608
|
||||
|
||||
- name: Install initial workforce policy when absent
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ detmir_portal_workforce_policy_path }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
force: false
|
||||
content: "{{ lookup('file', aw_repo_root + '/configs/detmir-workforce-policy.example.json') }}"
|
||||
|
||||
- name: Ensure detmir-portal state and evidence directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"default_role": "accountant",
|
||||
"roles": {
|
||||
"accountant": {
|
||||
"label": "Бухгалтер",
|
||||
"planned_hours_per_day": 8,
|
||||
"default_weight": 0.2,
|
||||
"application_weights": {
|
||||
"1с": 1.0,
|
||||
"excel": 0.9,
|
||||
"word": 0.7,
|
||||
"rdp": 0.8,
|
||||
"browser": 0.4,
|
||||
"youtube": 0.0
|
||||
}
|
||||
},
|
||||
"sales": {
|
||||
"label": "Продажи",
|
||||
"planned_hours_per_day": 8,
|
||||
"default_weight": 0.3,
|
||||
"application_weights": {
|
||||
"crm": 1.0,
|
||||
"1с": 0.8,
|
||||
"browser": 0.8,
|
||||
"mail": 0.8,
|
||||
"telegram": 0.4,
|
||||
"youtube": 0.0
|
||||
}
|
||||
},
|
||||
"developer": {
|
||||
"label": "Разработчик",
|
||||
"planned_hours_per_day": 8,
|
||||
"default_weight": 0.3,
|
||||
"application_weights": {
|
||||
"ide": 1.0,
|
||||
"code": 1.0,
|
||||
"terminal": 0.9,
|
||||
"browser": 0.7,
|
||||
"git": 0.9,
|
||||
"youtube": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,52 @@ Forensics усиливают продукт, но не должны перетя
|
||||
|
||||
Основные KPI:
|
||||
|
||||
- полезная активность;
|
||||
- индекс активности: proxy `активное время / плановое рабочее время`;
|
||||
- взвешенная активность: только при настроенной role/application policy;
|
||||
- активное время;
|
||||
- простой;
|
||||
- число рабочих сессий;
|
||||
- активные приложения;
|
||||
- индекс полезной активности;
|
||||
- документы/операции 1С при наличии связанного бизнес-слоя.
|
||||
|
||||
Важно: термин "полезная активность" нельзя использовать для простого proxy по
|
||||
времени. Он допустим только для будущего/настроенного слоя, где приложения
|
||||
взвешены по ролям: например, для бухгалтера `1C` имеет высокий вес, а для
|
||||
маркетолога браузер и соцсети могут быть частью рабочей активности.
|
||||
|
||||
### Настройка весов приложений
|
||||
|
||||
Публичный пример:
|
||||
|
||||
- `configs/detmir-workforce-policy.example.json`
|
||||
|
||||
Runtime-файл на сервере портала:
|
||||
|
||||
- `/etc/detmir-portal-workforce-policy.json`
|
||||
|
||||
Правило:
|
||||
|
||||
- `default_role` задает роль для агрегированного отчета;
|
||||
- `planned_hours_per_day` задает плановый рабочий день для роли;
|
||||
- `default_weight` применяется к приложениям без явного правила;
|
||||
- `application_weights` задает веса приложений по подстроке имени.
|
||||
|
||||
Пример логики:
|
||||
|
||||
- бухгалтер: `1C = 1.0`, `Excel = 0.9`, `YouTube = 0.0`;
|
||||
- продажи: `CRM = 1.0`, `browser = 0.8`, `mail = 0.8`;
|
||||
- разработчик: `IDE/code = 1.0`, `terminal = 0.9`, `browser = 0.7`.
|
||||
|
||||
После изменения runtime-файла нужно перезапустить портал:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart detmir-portal.service
|
||||
```
|
||||
|
||||
Если policy-файл отсутствует, портал показывает только нейтральный
|
||||
`Индекс активности`. `Взвешенная активность` появляется только после настройки
|
||||
role/application policy.
|
||||
|
||||
Корректная формулировка для продажи:
|
||||
|
||||
> DetMir помогает руководителю видеть загрузку сотрудников и бизнес-процессов
|
||||
@@ -69,7 +107,7 @@ Forensics усиливают продукт, но не должны перетя
|
||||
|
||||
Порядок показа владельцу бизнеса:
|
||||
|
||||
1. Workforce: полезная активность, загрузка, RDP/1С, рабочие приложения.
|
||||
1. Workforce: индекс активности, загрузка, RDP/1С, рабочие приложения.
|
||||
2. Commercial reports: ежедневный срез, KPI, Markdown/HTML отчет.
|
||||
3. Security: DLP-сигналы и evidence.
|
||||
4. Forensics: цепочка расследования, Hayabusa, кейсы.
|
||||
|
||||
@@ -30,8 +30,13 @@ baseline и раздела `Phase 8: Post-MVP Enhancements`.
|
||||
- UI tab: `Отчеты`;
|
||||
- отчет содержит KPI для владельца/руководителя: worktime users, active time,
|
||||
active applications, DLP WARN/FAIL, evidence screenshots/items, open issues;
|
||||
- отчет и вкладка `Руководитель` показывают `Индекс полезной активности` как
|
||||
proxy `active time / 8h на сотрудника`;
|
||||
- отчет и вкладка `Руководитель` показывают `Индекс активности` как
|
||||
proxy `активное время / плановое рабочее время`;
|
||||
- отчет поддерживает `Взвешенную активность` при наличии
|
||||
`/etc/detmir-portal-workforce-policy.json`; публичный пример лежит в
|
||||
`configs/detmir-workforce-policy.example.json`;
|
||||
- Ansible устанавливает initial workforce policy только если runtime-файл
|
||||
отсутствует, чтобы не перетирать клиентские веса ролей;
|
||||
- отчет содержит Markdown export для передачи руководителю или заказчику;
|
||||
- формулировка DLP/case показателей зафиксирована как
|
||||
`derived detections/cases`, не как вручную подтвержденные инциденты;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
## 1. DetMir Workforce: трудоотдача и загрузка
|
||||
|
||||
Первый коммерческий экран для владельца и руководителя. Показывает не
|
||||
"сидел за компьютером", а полезную активность: кто реально работал, кто
|
||||
"сидел за компьютером", а активность: кто реально работал, кто
|
||||
перегружен, кто простаивает, сколько времени уходит в RDP, 1C и рабочие
|
||||
приложения.
|
||||
|
||||
@@ -99,7 +99,7 @@ Live endpoint:
|
||||
|
||||
## Рекомендуемый порядок демонстрации
|
||||
|
||||
1. `DetMir Workforce` - полезная активность, загрузка, RDP/1C и рабочие
|
||||
1. `DetMir Workforce` - активность, загрузка, RDP/1C и рабочие
|
||||
приложения.
|
||||
2. `RDP Worktime Report` - отдельный генерируемый per-user отчет как
|
||||
доказательство реальной работы сотрудников.
|
||||
|
||||
Reference in New Issue
Block a user