Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
web:
name: web (Ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm test -- --run
- run: pnpm build

rust-linux:
name: rust (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: install keyring deps
run: sudo apt-get install -y libdbus-1-dev
- run: cargo test --workspace
- run: cargo build -p projectus-server --release

rust-macos:
name: rust (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
- run: cargo build -p projectus-server --release
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ export function App() {
setOpenProject(null)
}

const dataRootLabel = workspace.capacidades?.data_root_label || undefined

return (
<Shell
screen={screen}
projectTitle={projectCard?.titulo}
config={workspace.config}
dataRootLabel={dataRootLabel}
onNavigate={navigate}
onSnapshotError={(text) => message('erro', text)}
>
Expand All @@ -86,7 +89,7 @@ export function App() {
{screen === 'ideias' && (
<IdeasView config={workspace.config} ideas={workspace.ideias} onIdeas={setIdeas} onMessage={message} />
)}
{screen === 'backup' && <BackupView config={workspace.config} onMessage={message} />}
{screen === 'backup' && <BackupView config={workspace.config} dataRootLabel={dataRootLabel} onMessage={message} />}
{screen === 'arquivo' && (
<ArchiveView board={workspace.board} ideas={workspace.ideias} onRefresh={refresh} onMessage={message} />
)}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/features/backup/BackupView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import type { Config, Snapshot } from '../../lib/types'

export function BackupView({
config,
dataRootLabel,
onMessage,
}: {
config: Config
dataRootLabel?: string
onMessage: (type: 'ok' | 'erro', text: string) => void
}) {
const [snapshots, setSnapshots] = useState<Snapshot[]>([])
Expand Down Expand Up @@ -45,7 +47,7 @@ export function BackupView({
/>
</header>
<p className="panel-copy">
cada snapshot envia a pasta integral <code>~/Documents/PROJECTUS</code>: configurações, kanbans, ideias,
cada snapshot envia a pasta integral <code>{dataRootLabel ?? '~/Documents/PROJECTUS'}</code>: configurações, kanbans, ideias,
Arquivo, históricos e anexos.
</p>
{!config.r2.configurado ? (
Expand Down
26 changes: 16 additions & 10 deletions apps/web/src/features/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,16 @@ export function SettingsView({
setDirty(true)
}

const credentialStore = credentialStatus?.store_nome ?? 'cofre do sistema'

const saveCredentials = async () => {
try {
await api.saveCredentials({ access_key_id: accessKey.trim(), secret_access_key: secretKey.trim() })
setAccessKey('')
setSecretKey('')
setCredentialStatus(await api.credentialStatus())
onMessage('ok', 'credenciais R2 fixadas no Keychain')
const updated = await api.credentialStatus()
setCredentialStatus(updated)
onMessage('ok', `credenciais R2 fixadas no ${updated.store_nome}`)
} catch (error) {
onMessage('erro', error instanceof Error ? error.message : 'não foi possível salvar credenciais')
}
Expand Down Expand Up @@ -300,10 +303,10 @@ export function SettingsView({
value={secretKey}
onChange={(event) => setSecretKey(event.target.value)}
/>
<small className="hint">logo abaixo, "Secret Access Key". Salva no Keychain do macOS.</small>
<small className="hint">logo abaixo, "Secret Access Key". Salva no {credentialStore}.</small>
</label>
<div className={credentialStatus?.fixadas ? 'credential-state credential-state--ok' : 'credential-state'}>
<strong>KEYCHAIN</strong>
<strong>{credentialStore.toUpperCase()}</strong>
<span>
{credentialStatus?.fixadas
? `credenciais fixadas (${credentialStatus.access_key_id_mascarada})`
Expand All @@ -316,7 +319,7 @@ export function SettingsView({
disabled={!accessKey.trim() || !secretKey.trim() || dirty || saving || !validR2Endpoint}
onClick={() => void saveCredentials()}
>
fixar credenciais no Keychain
fixar credenciais no {credentialStore}
</button>
</section>
<section className="panel">
Expand All @@ -334,19 +337,22 @@ export function SettingsView({
</p>
<p className="panel-copy">
{daemon?.instalado
? 'autostart instalado.'
? `autostart instalado via ${daemon.autostart_nome}.`
: daemon?.instalacao_disponivel
? 'autostart ainda não instalado.'
: 'para manter o backend ativo com a janela fechada, execute ./scripts/instalar-autostart.sh.'}
? `autostart ainda não instalado.`
: daemon?.suportado === false
? 'autostart não disponível nesta plataforma.'
: `para manter o backend ativo com a janela fechada, use o botão instalar${daemon?.instalacao_manual ? ` ou ${daemon.instalacao_manual}` : ''}.`}
</p>
{!daemon?.instalado && daemon?.instalacao_disponivel && (
<button
className="btn btn--quiet"
type="button"
onClick={async () => {
try {
setDaemon(await api.installDaemon())
onMessage('ok', 'autostart instalado no macOS')
const updated = await api.installDaemon()
setDaemon(updated)
onMessage('ok', `autostart instalado via ${updated.autostart_nome}`)
} catch (error) {
onMessage('erro', error instanceof Error ? error.message : 'não foi possível instalar')
}
Expand Down
16 changes: 14 additions & 2 deletions apps/web/src/features/shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export function Shell({
screen,
projectTitle,
config,
dataRootLabel,
onNavigate,
onSnapshotError,
children,
}: {
screen: Screen
projectTitle?: string
config: Config
dataRootLabel?: string
onNavigate: (screen: Screen) => void
onSnapshotError: (message: string) => void
children: ReactNode
Expand Down Expand Up @@ -71,8 +73,18 @@ export function Shell({
</ul>
<footer>
<span className="eyebrow">dados</span>
<div>~/Documents/</div>
<strong>PROJECTUS</strong>
{(() => {
const label = dataRootLabel ?? '~/Documents/PROJECTUS'
const slash = label.lastIndexOf('/')
const parent = slash >= 0 ? label.slice(0, slash + 1) : label + '/'
const base = slash >= 0 ? label.slice(slash + 1) : 'PROJECTUS'
return (
<>
<div>{parent}</div>
<strong>{base}</strong>
</>
)
})()}
<span className="eyebrow rail__section">backup</span>
<div className={config.r2.configurado ? 'status status--ok' : 'status'}>
{config.r2.configurado ? 'OK' : 'OFF'} / R2
Expand Down
14 changes: 12 additions & 2 deletions apps/web/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ export type ArchivedItem = {
arquivado_em: string
}
export type ArchiveIndex = { revision: number; itens: ArchivedItem[] }
export type ApiCapabilities = { api_version: number; arquivo: boolean; config_autosave: boolean }
export type ApiCapabilities = {
api_version: number
arquivo: boolean
config_autosave: boolean
data_root: string
data_root_label: string
}
export type Bootstrap = { config: Config; board: Board; ideias: Ideas; capacidades?: ApiCapabilities }
export type DocumentResponse<T> = { dados: T; markdown: string }
export type Snapshot = {
Expand All @@ -94,13 +100,17 @@ export type RemoteHistory = { snapshots: Snapshot[] }
export type BackupCredentialStatus = {
fixadas: boolean
access_key_id_mascarada: string | null
store_nome: string
}
export type DaemonStatus = {
suportado: boolean
instalado: boolean
instalacao_disponivel: boolean
plist: string | null
service_path: string | null
executavel: string
plataforma: string
autostart_nome: string
instalacao_manual: string | null
}
export type EntityCard = ProjectCard | TaskCard
export type LiveEvent = {
Expand Down
9 changes: 7 additions & 2 deletions crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ futures-core = "0.3"
hex = "0.4"
if-addrs = "0.13"
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp", "bmp"] }
keyring = { version = "3", features = ["apple-native"] }
libc = "0.2"
mime_guess = "2"
rand = "0.9"
serde = { version = "1", features = ["derive"] }
Expand All @@ -34,5 +32,12 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
walkdir = "2"

[target.'cfg(target_os = "macos")'.dependencies]
keyring = { version = "3", features = ["apple-native"] }
libc = "0.2"

[target.'cfg(target_os = "linux")'.dependencies]
keyring = { version = "3", features = ["sync-secret-service"] }

[dev-dependencies]
tempfile = "3"
Loading