Skip to content

Commit 318b387

Browse files
committed
fix time precision + fix heartbeat responses
1 parent 86b8784 commit 318b387

File tree

4 files changed

+52
-22
lines changed

4 files changed

+52
-22
lines changed

rustytime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rustytime/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "rustytime-server"
33
description = "🕒 blazingly fast time tracking for developers"
4-
version = "0.5.3"
4+
version = "0.5.4"
55
edition = "2024"
66
authors = ["ImShyMike"]
77
readme = "../README.md"

rustytime/src/handlers/user.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async fn process_heartbeat_request(
7171
data: heartbeat.into(),
7272
};
7373
let response_data = Json(HeartbeatApiResponseVariant::Single(response));
74-
Ok((StatusCode::CREATED, response_data).into_response())
74+
Ok((StatusCode::ACCEPTED, response_data).into_response())
7575
} else {
7676
Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
7777
.into_response())
@@ -91,11 +91,15 @@ async fn process_heartbeat_request(
9191
match store_heartbeats_in_db(&app_state.db_pool, new_heartbeats).await {
9292
Ok(heartbeats) => {
9393
if heartbeats.is_empty() {
94-
let response_data = Json(HeartbeatApiResponseVariant::Multiple(vec![]));
94+
let response_data = Json(HeartbeatApiResponseVariant::Multiple(
95+
HeartbeatBulkApiResponse { responses: vec![] },
96+
));
9597
Ok((StatusCode::CREATED, response_data).into_response())
9698
} else {
9799
let response_data = Json(HeartbeatApiResponseVariant::Multiple(
98-
heartbeats.into_iter().map(|h| h.into()).collect(),
100+
HeartbeatBulkApiResponse {
101+
responses: heartbeats.into_iter().map(|h| h.into()).collect(),
102+
},
99103
));
100104
Ok((StatusCode::CREATED, response_data).into_response())
101105
}

rustytime/src/models/heartbeat.rs

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use diesel::prelude::*;
44
use diesel::sql_types::{BigInt, Date, Nullable, Text};
55
use ipnetwork::IpNetwork;
66
use serde::{Deserialize, Serialize};
7+
use serde_json::Value;
78

89
use crate::schema::heartbeats;
910
use crate::utils::http::parse_user_agent;
@@ -161,23 +162,39 @@ pub struct HeartbeatRequest {
161162
#[serde(untagged)]
162163
pub enum HeartbeatApiResponseVariant {
163164
Single(HeartbeatApiResponse),
164-
Multiple(Vec<HeartbeatResponse>),
165+
Multiple(HeartbeatBulkApiResponse),
165166
}
166167

167168
#[derive(Serialize, Debug)]
168169
pub struct HeartbeatResponse {
169170
pub id: String,
170-
pub entity: String,
171-
#[serde(rename = "type")]
172-
pub type_: String,
173-
pub time: f64,
174171
}
175172

176173
#[derive(Serialize, Debug)]
177174
pub struct HeartbeatApiResponse {
178175
pub data: HeartbeatResponse,
179176
}
180177

178+
#[derive(Serialize, Debug)]
179+
pub struct HeartbeatBulkApiResponse {
180+
pub responses: Vec<BulkResponseItem>,
181+
}
182+
183+
#[derive(Serialize, Debug)]
184+
#[serde(untagged)]
185+
pub enum BulkResponsePayload {
186+
Data {
187+
data: HeartbeatResponse,
188+
},
189+
#[allow(dead_code)]
190+
Errors {
191+
errors: Value,
192+
},
193+
}
194+
195+
#[derive(Serialize, Debug)]
196+
pub struct BulkResponseItem(pub BulkResponsePayload, pub u16);
197+
181198
#[derive(Queryable, Selectable, Serialize, Deserialize, Debug, Clone)]
182199
#[diesel(table_name = heartbeats)]
183200
#[diesel(check_for_backend(diesel::pg::Pg))]
@@ -260,7 +277,11 @@ pub struct SanitizedHeartbeatRequest {
260277
impl SanitizedHeartbeatRequest {
261278
pub fn from_request(request: HeartbeatRequest) -> Self {
262279
// Convert timestamp (seconds since epoch) to DateTime<Utc>
263-
let time = DateTime::from_timestamp(request.time as i64, 0).unwrap_or_else(Utc::now);
280+
let time = DateTime::from_timestamp(
281+
request.time.trunc() as i64,
282+
(request.time.fract() * 1e9) as u32,
283+
)
284+
.unwrap_or_else(Utc::now);
264285

265286
// Handle test heartbeats
266287
let type_ = if request.entity == "test.txt" {
@@ -269,11 +290,17 @@ impl SanitizedHeartbeatRequest {
269290
request.type_
270291
};
271292

272-
// Fallback to "coding" category
293+
// Parse the category
273294
let category = if let Some(cat) = request.category {
274295
Some(cat)
275296
} else {
276-
Some("coding".to_string())
297+
if type_ == "domain" || type_ == "url" {
298+
Some("browsing".to_string())
299+
} else if type_ == "file" && request.language.is_some() {
300+
Some("coding".to_string())
301+
} else {
302+
None
303+
}
277304
};
278305

279306
// Convert dependencies and apply limits
@@ -412,22 +439,21 @@ impl NewHeartbeat {
412439
sanitized.into_new_heartbeat(user_id, ip_address, headers)
413440
}
414441
}
415-
416442
impl From<Heartbeat> for HeartbeatResponse {
417443
fn from(heartbeat: Heartbeat) -> Self {
418-
let id = heartbeat.id.to_string();
419-
// Convert DateTime<Utc> to timestamp (seconds since epoch)
420-
let time = heartbeat.time.timestamp() as f64;
421-
422444
Self {
423-
id,
424-
entity: heartbeat.entity,
425-
type_: heartbeat.type_,
426-
time,
445+
id: heartbeat.id.to_string(),
427446
}
428447
}
429448
}
430449

450+
impl From<Heartbeat> for BulkResponseItem {
451+
fn from(heartbeat: Heartbeat) -> Self {
452+
let response = HeartbeatResponse::from(heartbeat);
453+
BulkResponseItem(BulkResponsePayload::Data { data: response }, 201)
454+
}
455+
}
456+
431457
impl Heartbeat {
432458
pub fn count_total_heartbeats(conn: &mut PgConnection) -> QueryResult<i64> {
433459
heartbeats::table.count().get_result(conn)

0 commit comments

Comments
 (0)